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
}
}