// NpgsqlTypes\ArrayHandling.cs
//
// Author:
// Jon Hanna. (jon@hackcraft.net)
//
// Copyright (C) 2007-2008 The Npgsql Development Team
// npgsql-general@gborg.postgresql.org
// http://gborg.postgresql.org/project/npgsql/projdisplay.php
//
// 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.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace NpgsqlTypes
{
///
/// Handles serialisation of .NET array or IEnumeration to pg format.
/// Arrays of arrays, enumerations of enumerations, arrays of enumerations etc.
/// are treated as multi-dimensional arrays (in much the same manner as an array of arrays
/// is used to emulate multi-dimensional arrays in languages that lack native support for them).
/// If such an enumeration of enumerations is "jagged" (as opposed to rectangular, cuboid,
/// hypercuboid, hyperhypercuboid, etc) then this class will "correctly" serialise it, but pg
/// will raise an error as it doesn't allow jagged arrays.
///
internal class ArrayNativeToBackendTypeConverter
{
private readonly NpgsqlNativeTypeInfo _elementConverter;
///
/// Create an ArrayNativeToBackendTypeConverter with the element converter passed
///
/// The that would be used to serialise the element type.
public ArrayNativeToBackendTypeConverter(NpgsqlNativeTypeInfo elementConverter)
{
_elementConverter = elementConverter;
}
///
/// Serialise the enumeration or array.
///
public string FromArray(NpgsqlNativeTypeInfo TypeInfo, object NativeData, Boolean ForExtendedQuery)
{
if (ForExtendedQuery)
{
StringBuilder sb = new StringBuilder("{");
//return sb.ToString();
WriteItem(TypeInfo, NativeData, sb, ForExtendedQuery);
sb.Append("}");
return sb.ToString();
}
else
{
//just prepend "array" and then pass to WriteItem.
StringBuilder sb = new StringBuilder("array");
if (WriteItem(TypeInfo, NativeData, sb, ForExtendedQuery))
{
return sb.ToString();
}
else
{
return "'{}'";
}
}
}
private bool WriteItem(NpgsqlNativeTypeInfo TypeInfo, object item, StringBuilder sb, Boolean ForExtendedQuery)
{
//item could be:
//an Ienumerable - in which case we call WriteEnumeration
//an element - in which case we call the NpgsqlNativeTypeInfo for that type to serialise it.
//an array - in which case we call WriteArray,
// Even an string being an IEnumerable, it shouldn't be processed. It will be processed on the last else.
// See http://pgfoundry.org/tracker/?func=detail&atid=592&aid=1010514&group_id=1000140 for more info.
if(item == null || NpgsqlTypesHelper.DefinedType(item))
{
sb.Append(_elementConverter.ConvertToBackend(item, ForExtendedQuery));
return true;
}
else if (item is Array)
{
return WriteArray(TypeInfo, item as Array, sb, ForExtendedQuery);
}
else if (item is IEnumerable)
{
return WriteEnumeration(TypeInfo, item as IEnumerable, sb, ForExtendedQuery);
}
else
{//This shouldn't really be reachable.
sb.Append(_elementConverter.ConvertToBackend(item, ForExtendedQuery));
return true;
}
}
private bool WriteArray(NpgsqlNativeTypeInfo TypeInfo, Array ar, StringBuilder sb, Boolean ForExtendedQuery)
{
bool writtenSomething = false;
//we need to know the size of each dimension.
int c = ar.Rank;
List lengths = new List(c);
do
{
lengths.Add(ar.GetLength(--c));
}
while (c != 0);
//c is now zero. Might as well reuse it!
foreach (object item in ar)
{
// As this prcedure handles both prepared and plain query representations, in order to not keep if's inside the loops
// we simply set a placeholder here for both openElement ( '{' or '[' ) and closeElement ( '}', or ']' )
Char openElement = ForExtendedQuery ? '{' : '[';
Char closeElement = ForExtendedQuery ? '}' : ']';
//to work out how many [ characters we need we need to work where we are compared to the dimensions.
//Say we are at position 24 in a 3 * 4 * 5 array.
//We're at the end of a row as 24 % 3 == 0 so write one [ for that.
//We're at the end of a square as 24 % (3 * 4) == 24 % (12) == 0 so write one [ for that.
//We're not at the end of a cube as 24 % (3 * 4 * 5) == 24 % (30) != 0, so we're finished for that pass.
int curlength = 1;
foreach (int lengthTest in lengths)
{
if (c%(curlength *= lengthTest) == 0)
{
//sb.Append('[');
sb.Append(openElement);
}
else
{
break;
}
}
//Write whatever the element is.
writtenSomething |= WriteItem(TypeInfo, item, sb, ForExtendedQuery);
++c; //up our counter for knowing when to write [ and ]
//same logic as above for writing [ this time writing ]
curlength = 1;
foreach (int lengthTest in lengths)
{
if (c%(curlength *= lengthTest) == 0)
{
//sb.Append(']');
sb.Append(closeElement);
}
else
{
break;
}
}
//comma between each item.
sb.Append(',');
}
if (writtenSomething)
{
//last comma was one too many.
sb.Remove(sb.Length - 1, 1);
}
return writtenSomething;
}
private bool WriteEnumeration(NpgsqlNativeTypeInfo TypeInfo, IEnumerable col, StringBuilder sb, Boolean ForExtendedQuery)
{
// As this prcedure handles both prepared and plain query representations, in order to not keep if's inside the loops
// we simply set a placeholder here for both openElement ( '{' or '[' ) and closeElement ( '}', or ']' )
Char openElement = ForExtendedQuery ? '{' : '[';
Char closeElement = ForExtendedQuery ? '}' : ']';
bool writtenSomething = false;
//sb.Append('[');
sb.Append(openElement);
//write each item with a comma between them.
foreach (object item in col)
{
writtenSomething |= WriteItem(TypeInfo, item, sb, ForExtendedQuery);
sb.Append(',');
}
if (writtenSomething)
{
//last comma was one too many. Replace it with the final }
//sb[sb.Length - 1] = ']';
sb[sb.Length - 1] = closeElement;
}
return writtenSomething;
}
}
///
/// Handles parsing of pg arrays into .NET arrays.
///
internal class ArrayBackendToNativeTypeConverter
{
#region Helper Classes
///
/// Takes a string representation of a pg 1-dimensional array
/// (or a 1-dimensional row within an n-dimensional array)
/// and allows enumeration of the string represenations of each items.
///
private static IEnumerable TokenEnumeration(string source)
{
bool inQuoted = false;
StringBuilder sb = new StringBuilder(source.Length);
//We start of not in a quoted section, with an empty StringBuilder.
//We iterate through each character. Generally we add that character
//to the string builder, but in the case of special characters we
//have different handling. When we reach a comma that isn't in a quoted
//section we yield return the string we have built and then clear it
//to continue with the next.
for (int idx = 0; idx < source.Length; ++idx)
{
char c = source[idx];
switch (c)
{
case '"': //entering of leaving a quoted string
inQuoted = !inQuoted;
break;
case ',': //ending this item, unless we're in a quoted string.
if (inQuoted)
{
sb.Append(',');
}
else
{
yield return sb.ToString();
sb = new StringBuilder(source.Length - idx);
}
break;
case '\\': //next char is an escaped character, grab it, ignore the \ we are on now.
sb.Append(source[++idx]);
break;
default:
sb.Append(c);
break;
}
}
yield return sb.ToString();
}
///
/// Takes a string representation of a pg n-dimensional array
/// and allows enumeration of the string represenations of the next
/// lower level of rows (which in turn can be taken as (n-1)-dimensional arrays.
///
private static IEnumerable ArrayChunkEnumeration(string source)
{
//Iterate through the string counting { and } characters, mindful of the effects of
//" and \ on how the string is to be interpretted.
//Increment a count of braces at each { and decrement at each }
//If we hit a comma and the brace-count is zero then we have found an item. yield it
//and then we move on to the next.
bool inQuoted = false;
int braceCount = 0;
int startIdx = 0;
int len = source.Length;
for (int idx = 0; idx != len; ++idx)
{
switch (source[idx])
{
case '"': //beginning or ending a quoted chunk.
inQuoted = !inQuoted;
break;
case ',':
if (braceCount == 0) //if bracecount is zero we've done our chunk
{
yield return source.Substring(startIdx, idx - startIdx);
startIdx = idx + 1;
}
break;
case '\\': //next character is escaped. Skip it.
++idx;
break;
case '{': //up the brace count if this isn't in a quoted string
if (!inQuoted)
{
++braceCount;
}
break;
case '}': //lower the brace count if this isn't in a quoted string
if (!inQuoted)
{
--braceCount;
}
break;
}
}
yield return source.Substring(startIdx);
}
///
/// Takes an array of ints and treats them like the limits of a set of counters.
/// Retains a matching set of ints that is set to all zeros on the first ++
/// On a ++ it increments the "right-most" int. If that int reaches it's
/// limit it is set to zero and the one before it is incremented, and so on.
///
/// Making this a more general purpose class is pretty straight-forward, but we'll just put what we need here.
///
private class IntSetIterator
{
private readonly int[] _bounds;
private readonly int[] _current;
public IntSetIterator(int[] bounds)
{
_current = new int[(_bounds = bounds).Length];
_current[_current.Length - 1] = -1; //zero after first ++
}
public IntSetIterator(List bounds)
: this(bounds.ToArray())
{
}
public int[] Bounds
{
get { return _bounds; }
}
public static implicit operator int[](IntSetIterator isi)
{
return isi._current;
}
public static IntSetIterator operator ++(IntSetIterator isi)
{
for (int idx = isi._current.Length - 1; ++isi._current[idx] == isi._bounds[idx]; --idx)
{
isi._current[idx] = 0;
}
return isi;
}
}
///
/// Takes an ArrayList which may be an ArrayList of ArrayLists, an ArrayList of ArrayLists of ArrayLists
/// and so on and enumerates the items that aren't ArrayLists (the leaf nodes if we think of the ArrayList
/// passed as a tree). Simply uses the ArrayLists' own IEnumerators to get that of the next,
/// pushing them onto a stack until we hit something that isn't an ArrayList.
/// ArrayList to enumerate
/// IEnumerable
///
private static IEnumerable RecursiveArrayListEnumeration(ArrayList list)
{
//This sort of recursive enumeration used to be really fiddly. .NET2.0's yield makes it trivial. Hurray!
Stack stk = new Stack();
stk.Push(list.GetEnumerator());
while (stk.Count != 0)
{
IEnumerator ienum = stk.Peek();
while (ienum.MoveNext())
{
object obj = ienum.Current;
if (obj is ArrayList)
{
stk.Push(ienum = (obj as ArrayList).GetEnumerator());
}
else
{
yield return obj;
}
}
stk.Pop();
}
}
#endregion
private readonly NpgsqlBackendTypeInfo _elementConverter;
///
/// Create a new ArrayBackendToNativeTypeConverter
///
/// for the element type.
public ArrayBackendToNativeTypeConverter(NpgsqlBackendTypeInfo elementConverter)
{
_elementConverter = elementConverter;
}
///
/// Creates an array from pg representation.
///
public object ToArray(NpgsqlBackendTypeInfo TypeInfo, String BackendData, Int16 TypeSize, Int32 TypeModifier)
{
//first create an arraylist, then convert it to an array.
return ToArray(ToArrayList(TypeInfo, BackendData, TypeSize, TypeModifier), _elementConverter.Type);
}
///
/// Creates an array list from pg represenation of an array.
/// Multidimensional arrays are treated as ArrayLists of ArrayLists
///
private ArrayList ToArrayList(NpgsqlBackendTypeInfo TypeInfo, String BackendData, Int16 elementTypeSize,
Int32 elementTypeModifier)
{
ArrayList list = new ArrayList();
//remove the braces on either side and work on what they contain.
string stripBraces = BackendData.Trim().Substring(1, BackendData.Length - 2).Trim();
if (stripBraces.Length == 0)
{
return list;
}
if (stripBraces[0] == '{')
//there are still braces so we have an n-dimension array. Recursively build an ArrayList of ArrayLists
{
foreach (string arrayChunk in ArrayChunkEnumeration(stripBraces))
{
list.Add(ToArrayList(TypeInfo, arrayChunk, elementTypeSize, elementTypeModifier));
}
}
else
//We're either dealing with a 1-dimension array or treating a row of an n-dimension array. In either case parse the elements and put them in our ArrayList
{
foreach (string token in TokenEnumeration(stripBraces))
{
//Use the NpgsqlBackendTypeInfo for the element type to obtain each element.
list.Add(_elementConverter.ConvertToNative(token, elementTypeSize, elementTypeModifier));
}
}
return list;
}
///
/// Creates an n-dimensional array from an ArrayList of ArrayLists or
/// a 1-dimensional array from something else.
///
/// to convert
/// produced.
private static Array ToArray(ArrayList list, Type elementType)
{
//First we need to work out how many dimensions we're dealing with and what size each one is.
//We examine the arraylist we were passed for it's count. Then we recursively do that to
//it's first item until we hit an item that isn't an ArrayList.
//We then find out the type of that item.
List dimensions = new List();
object item = list;
for (ArrayList itemAsList = item as ArrayList; item is ArrayList; itemAsList = (item = itemAsList[0]) as ArrayList)
{
if (itemAsList != null)
{
int dimension = itemAsList.Count;
if (dimension == 0)
{
return Array.CreateInstance(elementType, 0);
}
dimensions.Add(dimension);
}
}
if (dimensions.Count == 1) //1-dimension array so we can just use ArrayList.ToArray()
{
return list.ToArray(elementType);
}
//Get an IntSetIterator to hold the position we're setting.
IntSetIterator isi = new IntSetIterator(dimensions);
//Create our array.
Array ret = Array.CreateInstance(elementType, isi.Bounds);
//Get each item and put it in the array at the appropriate place.
foreach (object val in RecursiveArrayListEnumeration(list))
{
ret.SetValue(val, ++isi);
}
return ret;
}
}
}