1 // NpgsqlTypes\ArrayHandling.cs
4 // Jon Hanna. (jon@hackcraft.net)
6 // Copyright (C) 2007-2008 The Npgsql Development Team
7 // npgsql-general@gborg.postgresql.org
8 // http://gborg.postgresql.org/project/npgsql/projdisplay.php
10 // Permission to use, copy, modify, and distribute this software and its
11 // documentation for any purpose, without fee, and without a written
12 // agreement is hereby granted, provided that the above copyright notice
13 // and this paragraph and the following two paragraphs appear in all copies.
15 // IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY
16 // FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
17 // INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
18 // DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF
19 // THE POSSIBILITY OF SUCH DAMAGE.
21 // THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES,
22 // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
23 // AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
24 // ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
25 // TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
28 using System
.Collections
;
29 using System
.Collections
.Generic
;
35 /// Handles serialisation of .NET array or IEnumeration to pg format.
36 /// Arrays of arrays, enumerations of enumerations, arrays of enumerations etc.
37 /// are treated as multi-dimensional arrays (in much the same manner as an array of arrays
38 /// is used to emulate multi-dimensional arrays in languages that lack native support for them).
39 /// If such an enumeration of enumerations is "jagged" (as opposed to rectangular, cuboid,
40 /// hypercuboid, hyperhypercuboid, etc) then this class will "correctly" serialise it, but pg
41 /// will raise an error as it doesn't allow jagged arrays.
43 internal class ArrayNativeToBackendTypeConverter
45 private readonly NpgsqlNativeTypeInfo _elementConverter
;
48 /// Create an ArrayNativeToBackendTypeConverter with the element converter passed
50 /// <param name="elementConverter">The <see cref="NpgsqlNativeTypeInfo"/> that would be used to serialise the element type.</param>
51 public ArrayNativeToBackendTypeConverter(NpgsqlNativeTypeInfo elementConverter
)
53 _elementConverter
= elementConverter
;
57 /// Serialise the enumeration or array.
59 public string FromArray(NpgsqlNativeTypeInfo TypeInfo
, object NativeData
)
61 //just prepend "array" and then pass to WriteItem.
62 StringBuilder sb
= new StringBuilder("array");
63 if (WriteItem(TypeInfo
, NativeData
, sb
))
73 private bool WriteItem(NpgsqlNativeTypeInfo TypeInfo
, object item
, StringBuilder sb
)
76 //an Ienumerable - in which case we call WriteEnumeration
77 //an element - in which case we call the NpgsqlNativeTypeInfo for that type to serialise it.
78 //an array - in which case we call WriteArray,
80 if (item
is IEnumerable
)
82 return WriteEnumeration(TypeInfo
, item
as IEnumerable
, sb
);
84 else if (item
is Array
)
86 return WriteArray(TypeInfo
, item
as Array
, sb
);
90 sb
.Append(_elementConverter
.ConvertToBackend(item
, false));
97 private bool WriteArray(NpgsqlNativeTypeInfo TypeInfo
, Array ar
, StringBuilder sb
)
99 bool writtenSomething
= false;
100 //we need to know the size of each dimension.
102 List
<int> lengths
= new List
<int>(c
);
105 lengths
.Add(ar
.GetLength(--c
));
109 //c is now zero. Might as well reuse it!
111 foreach (object item
in ar
)
113 //to work out how many [ characters we need we need to work where we are compared to the dimensions.
114 //Say we are at position 24 in a 3 * 4 * 5 array.
115 //We're at the end of a row as 24 % 3 == 0 so write one [ for that.
116 //We're at the end of a square as 24 % (3 * 4) == 24 % (12) == 0 so write one [ for that.
117 //We're not at the end of a cube as 24 % (3 * 4 * 5) == 24 % (30) != 0, so we're finished for that pass.
119 foreach (int lengthTest
in lengths
)
121 if (c
%(curlength
*= lengthTest
) == 0)
131 //Write whatever the element is.
132 writtenSomething
|= WriteItem(TypeInfo
, item
, sb
);
133 ++c
; //up our counter for knowing when to write [ and ]
135 //same logic as above for writing [ this time writing ]
137 foreach (int lengthTest
in lengths
)
139 if (c
%(curlength
*= lengthTest
) == 0)
149 //comma between each item.
152 if (writtenSomething
)
154 //last comma was one too many.
155 sb
.Remove(sb
.Length
- 1, 1);
157 return writtenSomething
;
160 private bool WriteEnumeration(NpgsqlNativeTypeInfo TypeInfo
, IEnumerable col
, StringBuilder sb
)
162 bool writtenSomething
= false;
164 //write each item with a comma between them.
165 foreach (object item
in col
)
167 writtenSomething
|= WriteItem(TypeInfo
, item
, sb
);
170 if (writtenSomething
)
172 //last comma was one too many. Replace it with the final }
173 sb
[sb
.Length
- 1] = ']';
175 return writtenSomething
;
180 /// Handles parsing of pg arrays into .NET arrays.
182 internal class ArrayBackendToNativeTypeConverter
184 #region Helper Classes
187 /// Takes a string representation of a pg 1-dimensional array
188 /// (or a 1-dimensional row within an n-dimensional array)
189 /// and allows enumeration of the string represenations of each items.
191 private static IEnumerable
<string> TokenEnumeration(string source
)
193 bool inQuoted
= false;
194 StringBuilder sb
= new StringBuilder(source
.Length
);
195 //We start of not in a quoted section, with an empty StringBuilder.
196 //We iterate through each character. Generally we add that character
197 //to the string builder, but in the case of special characters we
198 //have different handling. When we reach a comma that isn't in a quoted
199 //section we yield return the string we have built and then clear it
200 //to continue with the next.
201 for (int idx
= 0; idx
< source
.Length
; ++idx
)
203 char c
= source
[idx
];
206 case '"': //entering of leaving a quoted string
207 inQuoted
= !inQuoted
;
209 case ',': //ending this item, unless we're in a quoted string.
216 yield return sb
.ToString();
217 sb
= new StringBuilder(source
.Length
- idx
);
220 case '\\': //next char is an escaped character, grab it, ignore the \ we are on now.
221 sb
.Append(source
[++idx
]);
228 yield return sb
.ToString();
232 /// Takes a string representation of a pg n-dimensional array
233 /// and allows enumeration of the string represenations of the next
234 /// lower level of rows (which in turn can be taken as (n-1)-dimensional arrays.
236 private static IEnumerable
<string> ArrayChunkEnumeration(string source
)
238 //Iterate through the string counting { and } characters, mindful of the effects of
239 //" and \ on how the string is to be interpretted.
240 //Increment a count of braces at each { and decrement at each }
241 //If we hit a comma and the brace-count is zero then we have found an item. yield it
242 //and then we move on to the next.
243 bool inQuoted
= false;
246 int len
= source
.Length
;
247 for (int idx
= 0; idx
!= len
; ++idx
)
251 case '"': //beginning or ending a quoted chunk.
252 inQuoted
= !inQuoted
;
255 if (braceCount
== 0) //if bracecount is zero we've done our chunk
257 yield return source
.Substring(startIdx
, idx
- startIdx
);
261 case '\\': //next character is escaped. Skip it.
264 case '{': //up the brace count if this isn't in a quoted string
270 case '}': //lower the brace count if this isn't in a quoted string
278 yield return source
.Substring(startIdx
);
282 /// Takes an array of ints and treats them like the limits of a set of counters.
283 /// Retains a matching set of ints that is set to all zeros on the first ++
284 /// On a ++ it increments the "right-most" int. If that int reaches it's
285 /// limit it is set to zero and the one before it is incremented, and so on.
287 /// Making this a more general purpose class is pretty straight-forward, but we'll just put what we need here.
289 private class IntSetIterator
291 private readonly int[] _bounds
;
292 private readonly int[] _current
;
294 public IntSetIterator(int[] bounds
)
296 _current
= new int[(_bounds
= bounds
).Length
];
297 _current
[_current
.Length
- 1] = -1; //zero after first ++
300 public IntSetIterator(List
<int> bounds
)
301 : this(bounds
.ToArray())
307 get { return _bounds; }
310 public static implicit operator int[](IntSetIterator isi
)
315 public static IntSetIterator
operator ++(IntSetIterator isi
)
317 for (int idx
= isi
._current
.Length
- 1; ++isi
._current
[idx
] == isi
._bounds
[idx
]; --idx
)
319 isi
._current
[idx
] = 0;
326 /// Takes an ArrayList which may be an ArrayList of ArrayLists, an ArrayList of ArrayLists of ArrayLists
327 /// and so on and enumerates the items that aren't ArrayLists (the leaf nodes if we think of the ArrayList
328 /// passed as a tree). Simply uses the ArrayLists' own IEnumerators to get that of the next,
329 /// pushing them onto a stack until we hit something that isn't an ArrayList.
330 /// <param name="list"><see cref="System.Collections.ArrayList">ArrayList</see> to enumerate</param>
331 /// <returns><see cref="System.Collections.IEnumerable">IEnumerable</see></returns>
333 private static IEnumerable
RecursiveArrayListEnumeration(ArrayList list
)
335 //This sort of recursive enumeration used to be really fiddly. .NET2.0's yield makes it trivial. Hurray!
336 Stack
<IEnumerator
> stk
= new Stack
<IEnumerator
>();
337 stk
.Push(list
.GetEnumerator());
338 while (stk
.Count
!= 0)
340 IEnumerator ienum
= stk
.Peek();
341 while (ienum
.MoveNext())
343 object obj
= ienum
.Current
;
344 if (obj
is ArrayList
)
346 stk
.Push(ienum
= (obj
as ArrayList
).GetEnumerator());
359 private readonly NpgsqlBackendTypeInfo _elementConverter
;
362 /// Create a new ArrayBackendToNativeTypeConverter
364 /// <param name="elementConverter"><see cref="NpgsqlBackendTypeInfo"/> for the element type.</param>
365 public ArrayBackendToNativeTypeConverter(NpgsqlBackendTypeInfo elementConverter
)
367 _elementConverter
= elementConverter
;
371 /// Creates an array from pg representation.
373 public object ToArray(NpgsqlBackendTypeInfo TypeInfo
, String BackendData
, Int16 TypeSize
, Int32 TypeModifier
)
375 //first create an arraylist, then convert it to an array.
376 return ToArray(ToArrayList(TypeInfo
, BackendData
, TypeSize
, TypeModifier
), _elementConverter
.Type
);
380 /// Creates an array list from pg represenation of an array.
381 /// Multidimensional arrays are treated as ArrayLists of ArrayLists
383 private ArrayList
ToArrayList(NpgsqlBackendTypeInfo TypeInfo
, String BackendData
, Int16 elementTypeSize
,
384 Int32 elementTypeModifier
)
386 ArrayList list
= new ArrayList();
387 //remove the braces on either side and work on what they contain.
388 string stripBraces
= BackendData
.Trim().Substring(1, BackendData
.Length
- 2).Trim();
389 if (stripBraces
.Length
== 0)
393 if (stripBraces
[0] == '{')
394 //there are still braces so we have an n-dimension array. Recursively build an ArrayList of ArrayLists
396 foreach (string arrayChunk
in ArrayChunkEnumeration(stripBraces
))
398 list
.Add(ToArrayList(TypeInfo
, arrayChunk
, elementTypeSize
, elementTypeModifier
));
402 //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
404 foreach (string token
in TokenEnumeration(stripBraces
))
406 //Use the NpgsqlBackendTypeInfo for the element type to obtain each element.
407 list
.Add(_elementConverter
.ConvertToNative(token
, elementTypeSize
, elementTypeModifier
));
414 /// Creates an n-dimensional array from an ArrayList of ArrayLists or
415 /// a 1-dimensional array from something else.
417 /// <param name="list"><see cref="ArrayList"/> to convert</param>
418 /// <returns><see cref="Array"/> produced.</returns>
419 private static Array
ToArray(ArrayList list
, Type elementType
)
421 //First we need to work out how many dimensions we're dealing with and what size each one is.
422 //We examine the arraylist we were passed for it's count. Then we recursively do that to
423 //it's first item until we hit an item that isn't an ArrayList.
424 //We then find out the type of that item.
425 List
<int> dimensions
= new List
<int>();
427 for (ArrayList itemAsList
= item
as ArrayList
; item
is ArrayList
; itemAsList
= (item
= itemAsList
[0]) as ArrayList
)
429 if (itemAsList
!= null)
431 int dimension
= itemAsList
.Count
;
434 return Array
.CreateInstance(elementType
, 0);
436 dimensions
.Add(dimension
);
440 if (dimensions
.Count
== 1) //1-dimension array so we can just use ArrayList.ToArray()
442 return list
.ToArray(elementType
);
445 //Get an IntSetIterator to hold the position we're setting.
446 IntSetIterator isi
= new IntSetIterator(dimensions
);
448 Array ret
= Array
.CreateInstance(elementType
, isi
.Bounds
);
449 //Get each item and put it in the array at the appropriate place.
450 foreach (object val
in RecursiveArrayListEnumeration(list
))
452 ret
.SetValue(val
, ++isi
);