using Npgsql.Internal; using Npgsql.Internal.TypeHandlers; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; using Npgsql.TypeMapping; using System; using System.Collections; using System.Collections.Generic; using System.Data.Common; using System.Globalization; using System.IO; using System.Runtime.CompilerServices; namespace Npgsql; /// /// Reads a forward-only stream of rows from a nested data source. /// Can be retrieved using or /// . /// public sealed class NpgsqlNestedDataReader : DbDataReader { readonly NpgsqlDataReader _outermostReader; ulong _uniqueOutermostReaderRowId; readonly NpgsqlNestedDataReader? _outerNestedReader; NpgsqlNestedDataReader? _cachedFreeNestedDataReader; PostgresCompositeType? _compositeType; readonly int _depth; int _numRows; int _nextRowIndex; int _nextRowBufferPos; ReaderState _readerState; readonly List _columns = new(); readonly struct ColumnInfo { public readonly uint TypeOid; public readonly int BufferPos; public readonly NpgsqlTypeHandler TypeHandler; public ColumnInfo(uint typeOid, int bufferPos, NpgsqlTypeHandler typeHandler) { TypeOid = typeOid; BufferPos = bufferPos; TypeHandler = typeHandler; } } NpgsqlReadBuffer Buffer => _outermostReader.Buffer; ConnectorTypeMapper TypeMapper => _outermostReader.Connector.TypeMapper; internal NpgsqlNestedDataReader(NpgsqlDataReader outermostReader, NpgsqlNestedDataReader? outerNestedReader, ulong uniqueOutermostReaderRowId, int depth, PostgresCompositeType? compositeType) { _outermostReader = outermostReader; _outerNestedReader = outerNestedReader; _uniqueOutermostReaderRowId = uniqueOutermostReaderRowId; _depth = depth; _compositeType = compositeType; } internal void Init(ulong uniqueOutermostReaderRowId, PostgresCompositeType? compositeType) { _uniqueOutermostReaderRowId = uniqueOutermostReaderRowId; _columns.Clear(); _numRows = 0; _nextRowIndex = 0; _nextRowBufferPos = 0; _readerState = ReaderState.BeforeFirstRow; _compositeType = compositeType; } internal void InitArray() { var dimensions = Buffer.ReadInt32(); var containsNulls = Buffer.ReadInt32() == 1; Buffer.ReadUInt32(); // Element OID. Ignored. if (containsNulls) throw new InvalidOperationException("Record array contains null record"); if (dimensions == 0) return; if (dimensions != 1) throw new InvalidOperationException("Cannot read a multidimensional array with a nested DbDataReader"); _numRows = Buffer.ReadInt32(); Buffer.ReadInt32(); // Lower bound if (_numRows > 0) Buffer.ReadInt32(); // Length of first row _nextRowBufferPos = Buffer.ReadPosition; } internal void InitSingleRow() { _numRows = 1; _nextRowBufferPos = Buffer.ReadPosition; } /// public override object this[int ordinal] => GetValue(ordinal); /// public override object this[string name] => GetValue(GetOrdinal(name)); /// public override int Depth { get { CheckNotClosed(); return _depth; } } /// public override int FieldCount { get { CheckNotClosed(); return _readerState == ReaderState.OnRow ? _columns.Count : 0; } } /// public override bool HasRows { get { CheckNotClosed(); return _numRows > 0; } } /// public override bool IsClosed => _readerState == ReaderState.Closed || _readerState == ReaderState.Disposed || _outermostReader.IsClosed || _uniqueOutermostReaderRowId != _outermostReader.UniqueRowId; /// public override int RecordsAffected => -1; /// public override bool GetBoolean(int ordinal) => GetFieldValue(ordinal); /// public override byte GetByte(int ordinal) => GetFieldValue(ordinal); /// public override char GetChar(int ordinal) => GetFieldValue(ordinal); /// public override DateTime GetDateTime(int ordinal) => GetFieldValue(ordinal); /// public override decimal GetDecimal(int ordinal) => GetFieldValue(ordinal); /// public override double GetDouble(int ordinal) => GetFieldValue(ordinal); /// public override float GetFloat(int ordinal) => GetFieldValue(ordinal); /// public override Guid GetGuid(int ordinal) => GetFieldValue(ordinal); /// public override short GetInt16(int ordinal) => GetFieldValue(ordinal); /// public override int GetInt32(int ordinal) => GetFieldValue(ordinal); /// public override long GetInt64(int ordinal) => GetFieldValue(ordinal); /// public override string GetString(int ordinal) => GetFieldValue(ordinal); /// public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) { if (dataOffset < 0 || dataOffset > int.MaxValue) throw new ArgumentOutOfRangeException(nameof(dataOffset), dataOffset, $"dataOffset must be between {0} and {int.MaxValue}"); if (buffer != null && (bufferOffset < 0 || bufferOffset >= buffer.Length + 1)) throw new IndexOutOfRangeException($"bufferOffset must be between {0} and {(buffer.Length)}"); if (buffer != null && (length < 0 || length > buffer.Length - bufferOffset)) throw new IndexOutOfRangeException($"length must be between {0} and {buffer.Length - bufferOffset}"); var field = CheckRowAndColumnAndSeek(ordinal); var handler = field.Handler; if (!(handler is ByteaHandler)) throw new InvalidCastException("GetBytes() not supported for type " + field.Handler.PgDisplayName); if (field.Length == -1) throw new InvalidCastException("field is null"); var dataOffset2 = (int)dataOffset; if (dataOffset2 > field.Length) throw new ArgumentOutOfRangeException(nameof(dataOffset), $"attempting to read out of bounds from the column data, dataOffset must be between {0} and {field.Length}"); Buffer.ReadPosition += dataOffset2; length = Math.Min(length, field.Length - dataOffset2); if (buffer == null) return length; return Buffer.Read(new Span(buffer, bufferOffset, length)); } /// public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) => throw new NotSupportedException(); /// protected override DbDataReader GetDbDataReader(int ordinal) => GetData(ordinal); /// /// Returns a nested data reader for the requested column. /// The column type must be a record or a to Npgsql known composite type, or an array thereof. /// /// The zero-based column ordinal. /// A data reader. public new NpgsqlNestedDataReader GetData(int ordinal) { var field = CheckRowAndColumnAndSeek(ordinal); var type = field.Handler.PostgresType; var isArray = type is PostgresArrayType; var elementType = isArray ? ((PostgresArrayType)type).Element : type; var compositeType = elementType as PostgresCompositeType; if (elementType.InternalName != "record" && compositeType == null) throw new InvalidCastException("GetData() not supported for type " + type.DisplayName); if (field.Length == -1) throw new InvalidCastException("field is null"); var reader = _cachedFreeNestedDataReader; if (reader != null) { _cachedFreeNestedDataReader = null; reader.Init(_uniqueOutermostReaderRowId, compositeType); } else { reader = new NpgsqlNestedDataReader(_outermostReader, this, _uniqueOutermostReaderRowId, _depth + 1, compositeType); } if (isArray) reader.InitArray(); else reader.InitSingleRow(); return reader; } /// public override string GetDataTypeName(int ordinal) { var column = CheckRowAndColumn(ordinal); return column.TypeHandler.PgDisplayName; } /// public override IEnumerator GetEnumerator() => new DbEnumerator(this); /// public override string GetName(int ordinal) { CheckRowAndColumn(ordinal); return _compositeType?.Fields[ordinal].Name ?? "?column?"; } /// public override int GetOrdinal(string name) { if (_compositeType == null) throw new NotSupportedException("GetOrdinal is not supported for the record type"); for (var i = 0; i < _compositeType.Fields.Count; i++) { if (_compositeType.Fields[i].Name == name) return i; } for (var i = 0; i < _compositeType.Fields.Count; i++) { if (string.Compare(_compositeType.Fields[i].Name, name, CultureInfo.InvariantCulture, CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType) == 0) return i; } throw new IndexOutOfRangeException("Field not found in row: " + name); } /// public override Type GetFieldType(int ordinal) { var column = CheckRowAndColumn(ordinal); return column.TypeHandler.GetFieldType(); } /// public override object GetValue(int ordinal) { var column = CheckRowAndColumnAndSeek(ordinal); if (column.Length == -1) return DBNull.Value; return column.Handler.ReadAsObject(Buffer, column.Length); } /// public override int GetValues(object[] values) { if (values == null) throw new ArgumentNullException(nameof(values)); CheckOnRow(); var count = Math.Min(FieldCount, values.Length); for (var i = 0; i < count; i++) values[i] = GetValue(i); return count; } /// public override bool IsDBNull(int ordinal) => CheckRowAndColumnAndSeek(ordinal).Length == -1; /// public override T GetFieldValue(int ordinal) { if (typeof(T) == typeof(Stream)) return (T)(object)GetStream(ordinal); if (typeof(T) == typeof(TextReader)) return (T)(object)GetTextReader(ordinal); var field = CheckRowAndColumnAndSeek(ordinal); if (field.Length == -1) { // When T is a Nullable (and only in that case), we support returning null if (NullableHandler.Exists) return default!; if (typeof(T) == typeof(object)) return (T)(object)DBNull.Value; throw new InvalidCastException("field is null"); } return NullableHandler.Exists ? NullableHandler.Read(field.Handler, Buffer, field.Length, fieldDescription: null) : typeof(T) == typeof(object) ? (T)field.Handler.ReadAsObject(Buffer, field.Length, fieldDescription: null) : field.Handler.Read(Buffer, field.Length, fieldDescription: null); } /// public override Type GetProviderSpecificFieldType(int ordinal) { var column = CheckRowAndColumn(ordinal); return column.TypeHandler.GetProviderSpecificFieldType(); } /// public override object GetProviderSpecificValue(int ordinal) { var column = CheckRowAndColumnAndSeek(ordinal); if (column.Length == -1) return DBNull.Value; return column.Handler.ReadPsvAsObject(Buffer, column.Length); } /// public override int GetProviderSpecificValues(object[] values) { if (values == null) throw new ArgumentNullException(nameof(values)); CheckOnRow(); var count = Math.Min(FieldCount, values.Length); for (var i = 0; i < count; i++) values[i] = GetProviderSpecificValue(i); return count; } /// public override bool Read() { CheckResultSet(); Buffer.ReadPosition = _nextRowBufferPos; if (_nextRowIndex == _numRows) { _readerState = ReaderState.AfterRows; return false; } if (_nextRowIndex++ != 0) Buffer.ReadInt32(); // Length of record var numColumns = Buffer.ReadInt32(); for (var i = 0; i < numColumns; i++) { var typeOid = Buffer.ReadUInt32(); var bufferPos = Buffer.ReadPosition; if (i >= _columns.Count) _columns.Add(new ColumnInfo(typeOid, bufferPos, TypeMapper.ResolveByOID(typeOid))); else _columns[i] = new ColumnInfo(typeOid, bufferPos, _columns[i].TypeOid == typeOid ? _columns[i].TypeHandler : TypeMapper.ResolveByOID(typeOid)); var columnLen = Buffer.ReadInt32(); if (columnLen >= 0) Buffer.Skip(columnLen); } _columns.RemoveRange(numColumns, _columns.Count - numColumns); _nextRowBufferPos = Buffer.ReadPosition; _readerState = ReaderState.OnRow; return true; } /// public override bool NextResult() { CheckNotClosed(); _numRows = 0; _nextRowBufferPos = 0; _nextRowIndex = 0; _readerState = ReaderState.AfterResult; return false; } /// public override void Close() { if (_readerState != ReaderState.Disposed) { _readerState = ReaderState.Closed; } } /// protected override void Dispose(bool disposing) { if (disposing && _readerState != ReaderState.Disposed) { Close(); _readerState = ReaderState.Disposed; if (_outerNestedReader != null) { _outerNestedReader._cachedFreeNestedDataReader ??= this; } else { _outermostReader.CachedFreeNestedDataReader ??= this; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] void CheckNotClosed() { if (IsClosed) throw new InvalidOperationException("The reader is closed"); } [MethodImpl(MethodImplOptions.AggressiveInlining)] void CheckResultSet() { CheckNotClosed(); switch (_readerState) { case ReaderState.BeforeFirstRow: case ReaderState.OnRow: case ReaderState.AfterRows: break; default: throw new InvalidOperationException("No resultset is currently being traversed"); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] void CheckOnRow() { CheckResultSet(); if (_readerState != ReaderState.OnRow) throw new InvalidOperationException("No row is available"); } ColumnInfo CheckRowAndColumn(int column) { CheckOnRow(); if (column < 0 || column >= _columns.Count) throw new IndexOutOfRangeException($"Column must be between {0} and {_columns.Count - 1}"); return _columns[column]; } (NpgsqlTypeHandler Handler, int Length) CheckRowAndColumnAndSeek(int ordinal) { var column = CheckRowAndColumn(ordinal); Buffer.ReadPosition = column.BufferPos; var len = Buffer.ReadInt32(); return (column.TypeHandler, len); } enum ReaderState { BeforeFirstRow, OnRow, AfterRows, AfterResult, Closed, Disposed } }