using System; using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace Npgsql.Internal; [Experimental(NpgsqlDiagnostics.ConvertersExperimental)] public abstract class PgConverter { internal DbNullPredicate DbNullPredicateKind { get; } public bool IsDbNullable => DbNullPredicateKind is not DbNullPredicate.None; private protected PgConverter(Type type, bool isNullDefaultValue, bool customDbNullPredicate = false) => DbNullPredicateKind = customDbNullPredicate ? DbNullPredicate.Custom : InferDbNullPredicate(type, isNullDefaultValue); /// /// Whether this converter can handle the given format and with which buffer requirements. /// /// The data format. /// Returns the buffer requirements. /// Returns true if the given data format is supported. /// The buffer requirements should not cover database NULL reads or writes, these are handled by the caller. public abstract bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements); internal abstract Type TypeToConvert { get; } internal bool IsDbNullAsObject([NotNullWhen(false)] object? value, object? writeState) => DbNullPredicateKind switch { DbNullPredicate.Null => value is null, DbNullPredicate.None => false, DbNullPredicate.PolymorphicNull => value is null or DBNull, // We do the null check to keep the NotNullWhen(false) invariant. DbNullPredicate.Custom => IsDbNullValueAsObject(value, writeState) || (value is null && ThrowInvalidNullValue()), _ => ThrowDbNullPredicateOutOfRange() }; [Obsolete("Use the overload without ref.")] internal bool IsDbNullAsObject([NotNullWhen(false)] object? value, ref object? writeState) => IsDbNullAsObject(value, writeState); private protected abstract bool IsDbNullValueAsObject(object? value, object? writeState); [Obsolete("Use the overload without ref.")] private protected bool IsDbNullValueAsObject(object? value, ref object? writeState) => IsDbNullValueAsObject(value, writeState); internal abstract Size GetSizeAsObject(SizeContext context, object value, ref object? writeState); internal object ReadAsObject(PgReader reader) => ReadAsObject(async: false, reader, CancellationToken.None).GetAwaiter().GetResult(); internal ValueTask ReadAsObjectAsync(PgReader reader, CancellationToken cancellationToken = default) => ReadAsObject(async: true, reader, cancellationToken); // Shared sync/async abstract to reduce virtual method table size overhead and code size for each NpgsqlConverter instantiation. internal abstract ValueTask ReadAsObject(bool async, PgReader reader, CancellationToken cancellationToken); internal void WriteAsObject(PgWriter writer, object value) => WriteAsObject(async: false, writer, value, CancellationToken.None).GetAwaiter().GetResult(); internal ValueTask WriteAsObjectAsync(PgWriter writer, object value, CancellationToken cancellationToken = default) => WriteAsObject(async: true, writer, value, cancellationToken); // Shared sync/async abstract to reduce virtual method table size overhead and code size for each NpgsqlConverter instantiation. internal abstract ValueTask WriteAsObject(bool async, PgWriter writer, object value, CancellationToken cancellationToken); static DbNullPredicate InferDbNullPredicate(Type type, bool isNullDefaultValue) => type == typeof(object) || type == typeof(DBNull) ? DbNullPredicate.PolymorphicNull : isNullDefaultValue ? DbNullPredicate.Null : DbNullPredicate.None; internal enum DbNullPredicate : byte { /// Never DbNull (struct types) None, /// DbNull when *user code* Custom, /// DbNull when value is null Null, /// DbNull when value is null or DBNull PolymorphicNull } [DoesNotReturn] private protected void ThrowIORequired(Size bufferRequirement) => throw new InvalidOperationException($"Buffer requirement '{bufferRequirement}' not respected for converter '{GetType().FullName}', expected no IO to be required."); private protected static bool ThrowInvalidNullValue() => throw new ArgumentNullException("value", "Null value given for non-nullable type converter"); private protected bool ThrowDbNullPredicateOutOfRange() => throw new UnreachableException($"Unknown case {DbNullPredicateKind.ToString()}"); } public abstract class PgConverter : PgConverter { private protected PgConverter(bool customDbNullPredicate) : base(typeof(T), default(T) is null, customDbNullPredicate) { } #pragma warning disable CS0618 // Obsolete - delegates to ref overload for binary compat with existing overrides protected virtual bool IsDbNullValue(T? value, object? writeState) { // The obsolete ref overload is kept around for binary compatibility on the signature, but // mutating writeState during a null probe is no longer a supported behaviour. Detect the // mutation via a local captured before the forward and throw — a violating override is a // bug in the derived converter, not something to defend against here. var originalWriteState = writeState; var isDbNull = IsDbNullValue(value, ref writeState); if (!ReferenceEquals(writeState, originalWriteState)) ThrowHelper.ThrowInvalidOperationException( $"{GetType().FullName} mutated writeState from its IsDbNullValue override. Override the overload without ref and produce write state only in GetSize."); return isDbNull; } #pragma warning restore CS0618 [Obsolete("Use the overload without ref.")] [EditorBrowsable(EditorBrowsableState.Never)] protected virtual bool IsDbNullValue(T? value, ref object? writeState) => throw new NotSupportedException(); // Object null semantics as follows, if T is a struct (so excluding nullable) report false for null values, don't throw on the cast. // As a result this creates symmetry with IsDbNull when we're dealing with a struct T, as it cannot be passed null at all. private protected override bool IsDbNullValueAsObject(object? value, object? writeState) => (default(T) is null || value is not null) && IsDbNullValue((T?)value, writeState); public bool IsDbNull([NotNullWhen(false)] T? value, object? writeState) => DbNullPredicateKind switch { DbNullPredicate.Null => value is null, DbNullPredicate.None => false, DbNullPredicate.PolymorphicNull => value is null or DBNull, // We do the null check to keep the NotNullWhen(false) invariant. DbNullPredicate.Custom => IsDbNullValue(value, writeState) || (value is null && ThrowInvalidNullValue()), _ => ThrowDbNullPredicateOutOfRange() }; [Obsolete("Use the overload without ref.")] [EditorBrowsable(EditorBrowsableState.Never)] public bool IsDbNull([NotNullWhen(false)] T? value, ref object? writeState) => IsDbNull(value, writeState); public abstract T Read(PgReader reader); public abstract ValueTask ReadAsync(PgReader reader, CancellationToken cancellationToken = default); public abstract Size GetSize(SizeContext context, [DisallowNull]T value, ref object? writeState); public abstract void Write(PgWriter writer, [DisallowNull] T value); public abstract ValueTask WriteAsync(PgWriter writer, [DisallowNull] T value, CancellationToken cancellationToken = default); internal sealed override Type TypeToConvert => typeof(T); internal sealed override Size GetSizeAsObject(SizeContext context, object value, ref object? writeState) => GetSize(context, (T)value, ref writeState); } static class PgConverterExtensions { public static Size? GetSizeOrDbNull(this PgConverter converter, DataFormat format, Size writeRequirement, T? value, ref object? writeState) { if (converter.IsDbNull(value, writeState)) return null; if (writeRequirement is { Kind: SizeKind.Exact, Value: var byteCount }) return byteCount; var size = converter.GetSize(new(format, writeRequirement), value, ref writeState); switch (size.Kind) { case SizeKind.UpperBound: ThrowHelper.ThrowInvalidOperationException($"{nameof(SizeKind.UpperBound)} is not a valid return value for GetSize."); break; case SizeKind.Unknown: // Not valid yet. ThrowHelper.ThrowInvalidOperationException($"{nameof(SizeKind.Unknown)} is not a valid return value for GetSize."); break; } return size; } public static Size? GetSizeOrDbNullAsObject(this PgConverter converter, DataFormat format, Size writeRequirement, object? value, ref object? writeState) { if (converter.IsDbNullAsObject(value, writeState)) return null; if (writeRequirement is { Kind: SizeKind.Exact, Value: var byteCount }) return byteCount; var size = converter.GetSizeAsObject(new(format, writeRequirement), value, ref writeState); switch (size.Kind) { case SizeKind.UpperBound: ThrowHelper.ThrowInvalidOperationException($"{nameof(SizeKind.UpperBound)} is not a valid return value for GetSize."); break; case SizeKind.Unknown: // Not valid yet. ThrowHelper.ThrowInvalidOperationException($"{nameof(SizeKind.Unknown)} is not a valid return value for GetSize."); break; } return size; } internal static PgConverter UnsafeDowncast(this PgConverter converter) { // Justification: avoid perf cost of casting to a known base class type per read/write, see callers. Debug.Assert(converter is PgConverter); return Unsafe.As>(converter); } } [method: SetsRequiredMembers] public readonly struct SizeContext(DataFormat format, Size bufferRequirement) { public required Size BufferRequirement { get; init; } = bufferRequirement; public DataFormat Format { get; } = format; } class MultiWriteState : IDisposable { public ArrayPool<(Size Size, object? WriteState)>? ArrayPool { get; set; } public ArraySegment<(Size Size, object? WriteState)> Data { get; set; } public bool AnyWriteState { get; set; } public void Dispose() { if (Data.Array is not { } array) return; if (AnyWriteState) { for (var i = Data.Offset; i < Data.Offset + Data.Count; i++) if (array[i].WriteState is IDisposable disposable) disposable.Dispose(); Array.Clear(Data.Array, Data.Offset, Data.Count); } ArrayPool?.Return(Data.Array); } }