514 lines
16 KiB
C#
514 lines
16 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Reads a forward-only stream of rows from a nested data source.
|
|
/// Can be retrieved using <see cref="NpgsqlDataReader.GetData(int)"/> or
|
|
/// <see cref="NpgsqlNestedDataReader.GetData(int)"/>.
|
|
/// </summary>
|
|
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<ColumnInfo> _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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override object this[int ordinal] => GetValue(ordinal);
|
|
|
|
/// <inheritdoc />
|
|
public override object this[string name] => GetValue(GetOrdinal(name));
|
|
|
|
/// <inheritdoc />
|
|
public override int Depth
|
|
{
|
|
get
|
|
{
|
|
CheckNotClosed();
|
|
return _depth;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override int FieldCount
|
|
{
|
|
get
|
|
{
|
|
CheckNotClosed();
|
|
return _readerState == ReaderState.OnRow ? _columns.Count : 0;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool HasRows
|
|
{
|
|
get
|
|
{
|
|
CheckNotClosed();
|
|
return _numRows > 0;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool IsClosed
|
|
=> _readerState == ReaderState.Closed || _readerState == ReaderState.Disposed
|
|
|| _outermostReader.IsClosed || _uniqueOutermostReaderRowId != _outermostReader.UniqueRowId;
|
|
|
|
/// <inheritdoc />
|
|
public override int RecordsAffected => -1;
|
|
|
|
/// <inheritdoc />
|
|
public override bool GetBoolean(int ordinal) => GetFieldValue<bool>(ordinal);
|
|
/// <inheritdoc />
|
|
public override byte GetByte(int ordinal) => GetFieldValue<byte>(ordinal);
|
|
/// <inheritdoc />
|
|
public override char GetChar(int ordinal) => GetFieldValue<char>(ordinal);
|
|
/// <inheritdoc />
|
|
public override DateTime GetDateTime(int ordinal) => GetFieldValue<DateTime>(ordinal);
|
|
/// <inheritdoc />
|
|
public override decimal GetDecimal(int ordinal) => GetFieldValue<decimal>(ordinal);
|
|
/// <inheritdoc />
|
|
public override double GetDouble(int ordinal) => GetFieldValue<double>(ordinal);
|
|
/// <inheritdoc />
|
|
public override float GetFloat(int ordinal) => GetFieldValue<float>(ordinal);
|
|
/// <inheritdoc />
|
|
public override Guid GetGuid(int ordinal) => GetFieldValue<Guid>(ordinal);
|
|
/// <inheritdoc />
|
|
public override short GetInt16(int ordinal) => GetFieldValue<short>(ordinal);
|
|
/// <inheritdoc />
|
|
public override int GetInt32(int ordinal) => GetFieldValue<int>(ordinal);
|
|
/// <inheritdoc />
|
|
public override long GetInt64(int ordinal) => GetFieldValue<long>(ordinal);
|
|
/// <inheritdoc />
|
|
public override string GetString(int ordinal) => GetFieldValue<string>(ordinal);
|
|
|
|
/// <inheritdoc />
|
|
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<byte>(buffer, bufferOffset, length));
|
|
}
|
|
/// <inheritdoc />
|
|
public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length)
|
|
=> throw new NotSupportedException();
|
|
|
|
/// <inheritdoc />
|
|
protected override DbDataReader GetDbDataReader(int ordinal) => GetData(ordinal);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="ordinal">The zero-based column ordinal.</param>
|
|
/// <returns>A data reader.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override string GetDataTypeName(int ordinal)
|
|
{
|
|
var column = CheckRowAndColumn(ordinal);
|
|
return column.TypeHandler.PgDisplayName;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override IEnumerator GetEnumerator() => new DbEnumerator(this);
|
|
|
|
/// <inheritdoc />
|
|
public override string GetName(int ordinal)
|
|
{
|
|
CheckRowAndColumn(ordinal);
|
|
return _compositeType?.Fields[ordinal].Name ?? "?column?";
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override Type GetFieldType(int ordinal)
|
|
{
|
|
var column = CheckRowAndColumn(ordinal);
|
|
return column.TypeHandler.GetFieldType();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override object GetValue(int ordinal)
|
|
{
|
|
var column = CheckRowAndColumnAndSeek(ordinal);
|
|
if (column.Length == -1)
|
|
return DBNull.Value;
|
|
return column.Handler.ReadAsObject(Buffer, column.Length);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool IsDBNull(int ordinal)
|
|
=> CheckRowAndColumnAndSeek(ordinal).Length == -1;
|
|
|
|
/// <inheritdoc />
|
|
public override T GetFieldValue<T>(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<T> (and only in that case), we support returning null
|
|
if (NullableHandler<T>.Exists)
|
|
return default!;
|
|
|
|
if (typeof(T) == typeof(object))
|
|
return (T)(object)DBNull.Value;
|
|
|
|
throw new InvalidCastException("field is null");
|
|
}
|
|
|
|
return NullableHandler<T>.Exists
|
|
? NullableHandler<T>.Read(field.Handler, Buffer, field.Length, fieldDescription: null)
|
|
: typeof(T) == typeof(object)
|
|
? (T)field.Handler.ReadAsObject(Buffer, field.Length, fieldDescription: null)
|
|
: field.Handler.Read<T>(Buffer, field.Length, fieldDescription: null);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override Type GetProviderSpecificFieldType(int ordinal)
|
|
{
|
|
var column = CheckRowAndColumn(ordinal);
|
|
return column.TypeHandler.GetProviderSpecificFieldType();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override object GetProviderSpecificValue(int ordinal)
|
|
{
|
|
var column = CheckRowAndColumnAndSeek(ordinal);
|
|
if (column.Length == -1)
|
|
return DBNull.Value;
|
|
return column.Handler.ReadPsvAsObject(Buffer, column.Length);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool NextResult()
|
|
{
|
|
CheckNotClosed();
|
|
|
|
_numRows = 0;
|
|
_nextRowBufferPos = 0;
|
|
_nextRowIndex = 0;
|
|
_readerState = ReaderState.AfterResult;
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Close()
|
|
{
|
|
if (_readerState != ReaderState.Disposed)
|
|
{
|
|
_readerState = ReaderState.Closed;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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
|
|
}
|
|
} |