#region License
// The PostgreSQL License
//
// Copyright (C) 2015 The Npgsql Development Team
//
// Permission to use, copy, modify, and distribute this software and its
// documentation for any purpose, without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph and the following two paragraphs appear in all copies.
//
// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using JetBrains.Annotations;
using Npgsql.BackendMessages;
using NpgsqlTypes;
namespace Npgsql.TypeHandlers
{
internal interface IArrayHandler
{
Type GetElementFieldType(FieldDescription fieldDescription);
Type GetElementPsvType(FieldDescription fieldDescription);
}
///
/// Base class for all type handlers which handle PostgreSQL arrays.
///
///
/// http://www.postgresql.org/docs/current/static/arrays.html
///
internal class ArrayHandler : ChunkingTypeHandler, IArrayHandler
{
///
/// The lower bound value sent to the backend when writing arrays. Normally 1 (the PG default) but
/// is 0 for OIDVector.
///
protected int LowerBound { get; set; }
#region State
Array _readValue;
IList _writeValue;
ReadState _readState;
WriteState _writeState;
IEnumerator _enumerator;
NpgsqlBuffer _buf;
LengthCache _lengthCache;
FieldDescription _fieldDescription;
int _dimensions;
int[] _dimLengths, _indices;
int _index;
int _elementLen;
bool _wroteElementLen;
bool _preparedRead;
#endregion
internal override Type GetFieldType(FieldDescription fieldDescription)
{
return typeof (Array);
}
internal override Type GetProviderSpecificFieldType(FieldDescription fieldDescription)
{
return typeof (Array);
}
///
/// The type of the elements contained within this array
///
///
public virtual Type GetElementFieldType(FieldDescription fieldDescription)
{
return typeof(TElement);
}
///
/// The provider-specific type of the elements contained within this array,
///
///
public virtual Type GetElementPsvType(FieldDescription fieldDescription)
{
return typeof(TElement);
}
///
/// The type handler for the element that this array type holds
///
internal TypeHandler ElementHandler { get; private set; }
public ArrayHandler(TypeHandler elementHandler)
{
LowerBound = 1;
ElementHandler = elementHandler;
}
#region Read
public override void PrepareRead(NpgsqlBuffer buf, int len, FieldDescription fieldDescription = null)
{
Contract.Assert(_readState == ReadState.NeedPrepare);
if (_readState != ReadState.NeedPrepare) // Checks against recursion and bugs
throw new InvalidOperationException("Started reading a value before completing a previous value");
_buf = buf;
_fieldDescription = fieldDescription;
_elementLen = -1;
_readState = ReadState.ReadNothing;
_preparedRead = false;
}
public override bool Read(out Array result)
{
return Read(out result);
}
protected bool Read([CanBeNull] out Array result)
{
switch (_readState)
{
case ReadState.ReadNothing:
if (_buf.ReadBytesLeft < 12)
{
result = null;
return false;
}
_dimensions = _buf.ReadInt32();
_buf.ReadInt32(); // Has nulls. Not populated by PG?
var elementOID = _buf.ReadUInt32();
Contract.Assume(elementOID == ElementHandler.OID);
_dimLengths = new int[_dimensions];
if (_dimensions > 1) {
_indices = new int[_dimensions];
}
_index = 0;
_readState = ReadState.ReadHeader;
goto case ReadState.ReadHeader;
case ReadState.ReadHeader:
if (_buf.ReadBytesLeft < _dimensions * 8) {
result = null;
return false;
}
for (var i = 0; i < _dimensions; i++)
{
_dimLengths[i] = _buf.ReadInt32();
_buf.ReadInt32(); // We don't care about the lower bounds
}
if (_dimensions == 0)
{
result = new TElement2[0];
_readState = ReadState.NeedPrepare;
return true;
}
_readValue = Array.CreateInstance(typeof(TElement2), _dimLengths);
_readState = ReadState.ReadingElements;
goto case ReadState.ReadingElements;
case ReadState.ReadingElements:
var completed = _readValue is TElement2[]
? ReadElementsOneDimensional()
: ReadElementsMultidimensional();
if (!completed)
{
result = null;
return false;
}
result = _readValue;
_readValue = null;
_buf = null;
_fieldDescription = null;
_readState = ReadState.NeedPrepare;
return true;
default:
throw new ArgumentOutOfRangeException();
}
}
///
/// Optimized population for one-dimensional arrays without boxing/unboxing
///
bool ReadElementsOneDimensional()
{
var array = (TElement2[])_readValue;
for (; _index < array.Length; _index++)
{
TElement2 element;
if (!ReadSingleElement(out element)) { return false; }
array[_index] = element;
}
return true;
}
///
/// Recursively populates an array from PB binary data representation.
///
bool ReadElementsMultidimensional()
{
while (true)
{
TElement2 element;
if (!ReadSingleElement(out element)) { return false; }
_readValue.SetValue(element, _indices);
if (!MoveNextInMultidimensional()) { return true; }
}
}
bool MoveNextInMultidimensional()
{
_indices[_dimensions - 1]++;
for (var dim = _dimensions - 1; dim >= 0; dim--) {
if (_indices[dim] <= _readValue.GetUpperBound(dim)) {
continue;
}
if (dim == 0) {
return false;
}
for (var j = dim; j < _dimensions; j++)
_indices[j] = _readValue.GetLowerBound(j);
_indices[dim - 1]++;
}
return true;
}
bool ReadSingleElement(out TElement2 element)
{
try
{
if (_elementLen == -1)
{
if (_buf.ReadBytesLeft < 4)
{
element = default(TElement2);
return false;
}
_elementLen = _buf.ReadInt32();
if (_elementLen == -1)
{
// TODO: Nullables
element = default(TElement2);
return true;
}
}
var asSimpleReader = ElementHandler as ISimpleTypeHandler;
if (asSimpleReader != null)
{
if (_buf.ReadBytesLeft < _elementLen)
{
element = default(TElement2);
return false;
}
element = asSimpleReader.Read(_buf, _elementLen, _fieldDescription);
_elementLen = -1;
return true;
}
var asChunkingReader = ElementHandler as IChunkingTypeHandler;
if (asChunkingReader != null)
{
if (!_preparedRead)
{
asChunkingReader.PrepareRead(_buf, _elementLen, _fieldDescription);
_preparedRead = true;
}
if (!asChunkingReader.Read(out element))
{
return false;
}
_elementLen = -1;
_preparedRead = false;
return true;
}
throw PGUtil.ThrowIfReached();
}
catch (SafeReadException e)
{
// TODO: Implement safe reading for array: read all values to the end, only then raise the
// SafeReadException. For now, translate the safe exception to an unsafe one to break the connector.
throw e.InnerException;
}
}
enum ReadState
{
NeedPrepare,
ReadNothing,
ReadHeader,
ReadingElements,
}
#endregion
#region Write
public override void PrepareWrite(object value, NpgsqlBuffer buf, LengthCache lengthCache, NpgsqlParameter parameter=null)
{
Contract.Assert(_readState == ReadState.NeedPrepare);
if (_writeState != WriteState.NeedPrepare) // Checks against recursion and bugs
throw new InvalidOperationException("Started reading a value before completing a previous value");
_buf = buf;
_lengthCache = lengthCache;
var asArray = value as Array;
_writeValue = (IList)value;
_dimensions = asArray?.Rank ?? 1;
_index = 0;
_wroteElementLen = false;
_writeState = WriteState.WroteNothing;
}
public override bool Write(ref DirectBuffer directBuf)
{
return Write(ref directBuf);
}
public bool Write(ref DirectBuffer directBuf)
{
switch (_writeState)
{
case WriteState.WroteNothing:
var len =
4 + // ndim
4 + // has_nulls
4 + // element_oid
_dimensions * 8; // dim (4) + lBound (4)
if (_buf.WriteSpaceLeft < len) {
Contract.Assume(_buf.UsableSize >= len, "Buffer too small for header");
return false;
}
_buf.WriteInt32(_dimensions);
_buf.WriteInt32(1); // HasNulls=1. Not actually used by the backend.
_buf.WriteUInt32(ElementHandler.OID);
var asArray = _writeValue as Array;
if (asArray != null)
{
for (var i = 0; i < _dimensions; i++)
{
_buf.WriteInt32(asArray.GetLength(i));
_buf.WriteInt32(LowerBound); // We don't map .NET lower bounds to PG
}
}
else
{
_buf.WriteInt32(_writeValue.Count);
_buf.WriteInt32(LowerBound); // We don't map .NET lower bounds to PG
_enumerator = _writeValue.GetEnumerator();
}
var asGeneric = _writeValue as IList;
_enumerator = asGeneric?.GetEnumerator() ?? _writeValue.GetEnumerator();
if (!_enumerator.MoveNext()) {
goto case WriteState.Cleanup;
}
_writeState = WriteState.WritingElements;
goto case WriteState.WritingElements;
case WriteState.WritingElements:
var genericEnumerator = _enumerator as IEnumerator;
if (genericEnumerator != null)
{
// TODO: Actually call the element writer generically...!
do
{
if (!WriteSingleElement(genericEnumerator.Current, ref directBuf)) { return false; }
} while (genericEnumerator.MoveNext());
}
else
{
do {
if (!WriteSingleElement(_enumerator.Current, ref directBuf)) { return false; }
} while (_enumerator.MoveNext());
}
goto case WriteState.Cleanup;
case WriteState.Cleanup:
_writeValue = null;
_buf = null;
_writeState = WriteState.NeedPrepare;
return true;
default:
throw PGUtil.ThrowIfReached();
}
}
bool WriteSingleElement([CanBeNull] object element, ref DirectBuffer directBuf)
{
// TODO: Need generic version of this...
if (element == null || element is DBNull) {
if (_buf.WriteSpaceLeft < 4) {
return false;
}
_buf.WriteInt32(-1);
return true;
}
var asSimpleWriter = ElementHandler as ISimpleTypeHandler;
if (asSimpleWriter != null)
{
var elementLen = asSimpleWriter.ValidateAndGetLength(element, null);
if (_buf.WriteSpaceLeft < 4 + elementLen) { return false; }
_buf.WriteInt32(elementLen);
asSimpleWriter.Write(element, _buf, null);
return true;
}
var asChunkedWriter = ElementHandler as IChunkingTypeHandler;
if (asChunkedWriter != null)
{
if (!_wroteElementLen) {
if (_buf.WriteSpaceLeft < 4) {
return false;
}
_buf.WriteInt32(asChunkedWriter.ValidateAndGetLength(element, ref _lengthCache, null));
asChunkedWriter.PrepareWrite(element, _buf, _lengthCache, null);
_wroteElementLen = true;
}
if (!asChunkedWriter.Write(ref directBuf)) {
return false;
}
_wroteElementLen = false;
return true;
}
throw PGUtil.ThrowIfReached();
}
public override int ValidateAndGetLength(object value, ref LengthCache lengthCache, NpgsqlParameter parameter = null)
{
return ValidateAndGetLength(value, ref lengthCache, parameter);
}
public int ValidateAndGetLength(object value, ref LengthCache lengthCache, NpgsqlParameter parameter=null)
{
// Take care of single-dimensional arrays and generic IList
var asGenericList = value as IList;
if (asGenericList != null)
{
if (lengthCache == null) {
lengthCache = new LengthCache(1);
}
if (lengthCache.IsPopulated) {
return lengthCache.Get();
}
// Leave empty slot for the entire array length, and go ahead an populate the element slots
var pos = lengthCache.Position;
lengthCache.Set(0);
var lengthCache2 = lengthCache;
var len = 12 + (1 * 8) + asGenericList.Sum(e => 4 + GetSingleElementLength(e, ref lengthCache2, parameter));
lengthCache = lengthCache2;
return lengthCache.Lengths[pos] = len;
}
// Take care of multi-dimensional arrays and non-generic IList, we have no choice but to do
// boxing/unboxing
var asNonGenericList = value as IList;
if (asNonGenericList != null)
{
if (lengthCache == null) {
lengthCache = new LengthCache(1);
}
if (lengthCache.IsPopulated) {
return lengthCache.Get();
}
var asMultidimensional = value as Array;
var dimensions = asMultidimensional?.Rank ?? 1;
// Leave empty slot for the entire array length, and go ahead an populate the element slots
var pos = lengthCache.Position;
lengthCache.Set(0);
var lengthCache2 = lengthCache;
var len = 12 + (dimensions * 8) + asNonGenericList.Cast