2004-05-22 Francisco Figueiredo Jr. <fxjrlists@yahoo.com.br>
[mono.git] / mcs / class / Npgsql / NpgsqlTypes / NpgsqlTypesHelper.cs
1
2 // NpgsqlTypes.NpgsqlTypesHelper.cs
3 //
4 // Author:
5 //      Francisco Jr. (fxjrlists@yahoo.com.br)
6 //
7 //      Copyright (C) 2002 The Npgsql Development Team
8 //      npgsql-general@gborg.postgresql.org
9 //      http://gborg.postgresql.org/project/npgsql/projdisplay.php
10 //
11 // This library is free software; you can redistribute it and/or
12 // modify it under the terms of the GNU Lesser General Public
13 // License as published by the Free Software Foundation; either
14 // version 2.1 of the License, or (at your option) any later version.
15 //
16 // This library is distributed in the hope that it will be useful,
17 // but WITHOUT ANY WARRANTY; without even the implied warranty of
18 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19 // Lesser General Public License for more details.
20 //
21 // You should have received a copy of the GNU Lesser General Public
22 // License along with this library; if not, write to the Free Software
23 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
24
25 using System;
26 using System.Collections;
27 using System.Globalization;
28 using System.Data;
29 using System.Net;
30 using System.Text;
31 using System.IO;
32 using Npgsql;
33 using System.Resources;
34
35
36
37 /// <summary>
38 ///     This class contains helper methods for type conversion between
39 /// the .Net type system and postgresql.
40 /// </summary>
41 namespace NpgsqlTypes
42 {
43
44     /*internal struct NpgsqlTypeMapping
45     {
46       public String        _backendTypeName;
47       public Type          _frameworkType;
48       public Int32         _typeOid;
49       public NpgsqlDbType  _npgsqlDbType;
50       
51       public NpgsqlTypeMapping(String backendTypeName, Type frameworkType, Int32 typeOid, NpgsqlDbType npgsqlDbType)
52       {
53         _backendTypeName = backendTypeName;
54         _frameworkType = frameworkType;
55         _typeOid = typeOid;
56         _npgsqlDbType = npgsqlDbType;
57         
58       }
59     }*/
60
61
62     internal class NpgsqlTypesHelper
63     {
64
65         private static Hashtable _oidToNameMappings = new Hashtable();
66
67         // Logging related values
68         private static readonly String CLASSNAME = "NpgsqlDataReader";
69         private static ResourceManager resman = new ResourceManager(typeof(NpgsqlTypesHelper));
70
71         // From include/utils/datetime.h. Thanks to Carlos Guzman Alvarez
72         private static readonly DateTime postgresEpoch = new DateTime(2000, 1, 1);
73
74         private static readonly string[] DateFormats = new String[] 
75         {
76           "yyyy-MM-dd",
77         };
78
79         private static readonly string[] TimeFormats = new String[]
80         {
81           "HH:mm:ss.ffffff",
82           "HH:mm:ss.fffff",     
83           "HH:mm:ss.ffff",
84           "HH:mm:ss.fff",
85           "HH:mm:ss.ff",
86           "HH:mm:ss.f",
87           "HH:mm:ss",
88           "HH:mm:ss.ffffffzz",
89           "HH:mm:ss.fffffzz",   
90           "HH:mm:ss.ffffzz",
91           "HH:mm:ss.fffzz",
92           "HH:mm:ss.ffzz",
93           "HH:mm:ss.fzz",
94           "HH:mm:sszz"
95         };
96
97         private static readonly string[] DateTimeFormats = new String[] 
98         {
99           "yyyy-MM-dd HH:mm:ss.ffffff",
100           "yyyy-MM-dd HH:mm:ss.fffff",  
101           "yyyy-MM-dd HH:mm:ss.ffff",
102           "yyyy-MM-dd HH:mm:ss.fff",
103           "yyyy-MM-dd HH:mm:ss.ff",
104           "yyyy-MM-dd HH:mm:ss.f",
105           "yyyy-MM-dd HH:mm:ss",
106           "yyyy-MM-dd HH:mm:ss.ffffffzz",
107           "yyyy-MM-dd HH:mm:ss.fffffzz",        
108           "yyyy-MM-dd HH:mm:ss.ffffzz",
109           "yyyy-MM-dd HH:mm:ss.fffzz",
110           "yyyy-MM-dd HH:mm:ss.ffzz",
111           "yyyy-MM-dd HH:mm:ss.fzz",
112           "yyyy-MM-dd HH:mm:sszz"
113         };
114
115         public static String GetBackendTypeNameFromDbType(DbType dbType)
116         {
117             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "GetBackendTypeNameFromDbType");
118
119             switch (dbType)
120             {
121             case DbType.Binary:
122                 return "bytea";
123             case DbType.Boolean:
124                 return "bool";
125             case DbType.Single:
126                 return "float4";
127             case DbType.Double:
128                 return "float8";
129             case DbType.Int64:
130                 return "int8";
131             case DbType.Int32:
132                 return "int4";
133             case DbType.Decimal:
134                 return "numeric";
135             case DbType.Int16:
136                 return "int2";
137             case DbType.String:
138             case DbType.AnsiString:
139                 return "text";
140             case DbType.DateTime:
141                 return "timestamp";
142             case DbType.Date:
143                 return "date";
144             case DbType.Time:
145                 return "time";
146             default:
147                 throw new InvalidCastException(String.Format(resman.GetString("Exception_TypeNotSupported"), dbType));
148
149             }
150         }
151
152         public static Object ConvertBackendBytesToStytemType(Hashtable oidToNameMapping, Byte[] data, Encoding encoding, Int32 fieldValueSize, Int32 typeOid, Int32 typeModifier)
153         {
154             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ConvertBackendBytesToStytemType");
155             //[TODO] Find a way to eliminate this checking. It is just used at bootstrap time
156             // when connecting because we don't have yet loaded typeMapping. The switch below
157             // crashes with NullPointerReference when it can't find the typeOid.
158
159             if (!oidToNameMapping.ContainsKey(typeOid))
160                 return data;
161
162             switch ((DbType)oidToNameMapping[typeOid])
163             {
164             case DbType.Binary:
165                 return data;
166             case DbType.Boolean:
167                 return BitConverter.ToBoolean(data, 0);
168             case DbType.DateTime:
169                 return DateTime.MinValue.AddTicks(IPAddress.NetworkToHostOrder(BitConverter.ToInt64(data, 0)));
170
171             case DbType.Int16:
172                 return IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 0));
173             case DbType.Int32:
174                 return IPAddress.NetworkToHostOrder(BitConverter.ToInt32(data, 0));
175             case DbType.Int64:
176                 return IPAddress.NetworkToHostOrder(BitConverter.ToInt64(data, 0));
177             case DbType.String:
178             case DbType.AnsiString:
179             case DbType.StringFixedLength:
180                 return encoding.GetString(data, 0, fieldValueSize);
181             default:
182                 throw new InvalidCastException("Type not supported in binary format");
183             }
184
185
186         }
187
188
189         public static String ConvertNpgsqlParameterToBackendStringValue(NpgsqlParameter parameter, Boolean QuoteStrings)
190         {
191             // HACK (?)
192             // glenebob@nwlink.com 05/20/2004
193             // bool QuoteString is a bit of a hack.
194             // When using the version 3 extended query support, we do not need to do quoting of parameters.
195             // The backend handles that properly.
196
197             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ConvertNpgsqlParameterToBackendStringValue");
198
199             if ((parameter.Value == DBNull.Value) || (parameter.Value == null))
200                 return "Null";
201
202             switch(parameter.DbType)
203             {
204             case DbType.Binary:
205                                                                 if (QuoteStrings) {
206                     return "'" + ConvertByteArrayToBytea((Byte[])parameter.Value) + "'";
207                 } else {
208                     return ConvertByteArrayToBytea((Byte[])parameter.Value);
209                 }
210
211             case DbType.Boolean:
212             case DbType.Int64:
213             case DbType.Int32:
214             case DbType.Int16:
215                 return parameter.Value.ToString();
216
217             case DbType.Single:
218                 // To not have a value implicitly converted to float8, we add quotes.
219                 if (QuoteStrings) {
220                     return "'" + ((Single)parameter.Value).ToString(NumberFormatInfo.InvariantInfo) + "'";
221                 } else {
222                     return ((Single)parameter.Value).ToString(NumberFormatInfo.InvariantInfo);
223                 }
224
225             case DbType.Double:
226                 return ((Double)parameter.Value).ToString(NumberFormatInfo.InvariantInfo);
227
228             case DbType.Date:
229                 if (QuoteStrings) {
230                     return "'" + ((DateTime)parameter.Value).ToString("yyyy-MM-dd") + "'";
231                 } else {
232                     return ((DateTime)parameter.Value).ToString("yyyy-MM-dd");
233                 }
234
235             case DbType.DateTime:
236                 if (QuoteStrings) {
237                     return "'" + ((DateTime)parameter.Value).ToString("yyyy-MM-dd HH:mm:ss.fff") + "'";
238                 } else {
239                     return ((DateTime)parameter.Value).ToString("yyyy-MM-dd HH:mm:ss.fff");
240                 }
241
242             case DbType.Decimal:
243                 return ((Decimal)parameter.Value).ToString(NumberFormatInfo.InvariantInfo);
244
245             case DbType.String:
246             case DbType.AnsiString:
247             case DbType.StringFixedLength:
248                 if (QuoteStrings) {
249                     return "'" + parameter.Value.ToString().Replace("'", "\\'") + "'";
250                 } else {
251                     return parameter.Value.ToString();
252                                                                 }
253
254             case DbType.Time:
255                 if (QuoteStrings) {
256                     return "'" + ((DateTime)parameter.Value).ToString("HH:mm:ss.ffff") + "'";
257                 } else {
258                     return ((DateTime)parameter.Value).ToString("HH:mm:ss.ffff");
259                                                                 }
260
261             default:
262                 // This should not happen!
263                 throw new InvalidCastException(String.Format(resman.GetString("Exception_TypeNotSupported"), parameter.DbType));
264
265
266             }
267
268         }
269
270
271         ///<summary>
272         /// This method is responsible to convert the string received from the backend
273         /// to the corresponding NpgsqlType.
274         /// </summary>
275         ///
276         public static Object ConvertBackendStringToSystemType(Hashtable oidToNameMapping, String data, Int32 typeOid, Int32 typeModifier)
277         {
278             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ConvertBackendStringToSystemType");
279             //[TODO] Find a way to eliminate this checking. It is just used at bootstrap time
280             // when connecting because we don't have yet loaded typeMapping. The switch below
281             // crashes with NullPointerReference when it can't find the typeOid.
282
283             if (!oidToNameMapping.ContainsKey(typeOid))
284                 return data;
285
286             switch ((DbType)oidToNameMapping[typeOid])
287             {
288             case DbType.Binary:
289                 return ConvertByteAToByteArray(data);
290
291             case DbType.Boolean:
292                 return (data.ToLower() == "t" ? true : false);
293
294             case DbType.Single:
295                 return Single.Parse(data, NumberFormatInfo.InvariantInfo);
296
297             case DbType.Double:
298                 return Double.Parse(data, NumberFormatInfo.InvariantInfo);
299
300             case DbType.Int16:
301                 return Int16.Parse(data);
302             case DbType.Int32:
303                 return Int32.Parse(data);
304
305             case DbType.Int64:
306                 return Int64.Parse(data);
307
308             case DbType.Decimal:
309                 // Got this manipulation of typemodifier from jdbc driver - file AbstractJdbc1ResultSetMetaData.java.html method getColumnDisplaySize
310                 {
311                     typeModifier -= 4;
312                     //Console.WriteLine("Numeric from server: {0} digitos.digitos {1}.{2}", data, (typeModifier >> 16) & 0xffff, typeModifier & 0xffff);
313                     return Decimal.Parse(data, NumberFormatInfo.InvariantInfo);
314
315                 }
316
317             case DbType.DateTime:
318
319                 // Get the date time parsed in all expected formats for timestamp.
320                 return DateTime.ParseExact(data,
321                                            DateTimeFormats,
322                                            DateTimeFormatInfo.InvariantInfo,
323                                            DateTimeStyles.NoCurrentDateDefault | DateTimeStyles.AllowWhiteSpaces);
324
325             case DbType.Date:
326                 return DateTime.ParseExact(data,
327                                            DateFormats,
328                                            DateTimeFormatInfo.InvariantInfo,
329                                            DateTimeStyles.AllowWhiteSpaces);
330
331             case DbType.Time:
332
333                 return DateTime.ParseExact(data,
334                                            TimeFormats,
335                                            DateTimeFormatInfo.InvariantInfo,
336                                            DateTimeStyles.NoCurrentDateDefault | DateTimeStyles.AllowWhiteSpaces);
337
338             case DbType.String:
339             case DbType.AnsiString:
340             case DbType.StringFixedLength:
341                 return data;
342             default:
343                 throw new InvalidCastException(String.Format(resman.GetString("Exception_TypeNotSupported"),  oidToNameMapping[typeOid]));
344
345
346             }
347         }
348
349
350
351         ///<summary>
352         /// This method gets a type oid and return the equivalent
353         /// Npgsql type.
354         /// </summary>
355         ///
356
357         public static Type GetSystemTypeFromTypeOid(Hashtable oidToNameMapping, Int32 typeOid)
358         {
359             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "GetSystemTypeFromTypeOid");
360             // This method gets a db type identifier and return the equivalent
361             // system type.
362
363             //[TODO] Find a way to eliminate this checking. It is just used at bootstrap time
364             // when connecting because we don't have yet loaded typeMapping. The switch below
365             // crashes with NullPointerReference when it can't find the typeOid.
366
367
368
369             if (!oidToNameMapping.ContainsKey(typeOid))
370                 return Type.GetType("System.String");
371
372             switch ((DbType)oidToNameMapping[typeOid])
373             {
374             case DbType.Binary:
375                 return Type.GetType("System.Byte[]");
376             case DbType.Boolean:
377                 return Type.GetType("System.Boolean");
378             case DbType.Int16:
379                 return Type.GetType("System.Int16");
380             case DbType.Single:
381                 return Type.GetType("System.Single");
382             case DbType.Double:
383                 return Type.GetType("System.Double");
384             case DbType.Int32:
385                 return Type.GetType("System.Int32");
386             case DbType.Int64:
387                 return Type.GetType("System.Int64");
388             case DbType.Decimal:
389                 return Type.GetType("System.Decimal");
390             case DbType.DateTime:
391             case DbType.Date:
392             case DbType.Time:
393                 return Type.GetType("System.DateTime");
394             case DbType.String:
395             case DbType.AnsiString:
396             case DbType.StringFixedLength:
397                 return Type.GetType("System.String");
398             default:
399                 throw new InvalidCastException(String.Format(resman.GetString("Exception_TypeNotSupported"), oidToNameMapping[typeOid]));
400
401             }
402
403
404         }
405
406
407         ///<summary>
408         /// This method is responsible to send query to get the oid-to-name mapping.
409         /// This is needed as from one version to another, this mapping can be changed and
410         /// so we avoid hardcoding them.
411         /// </summary>
412         public static Hashtable LoadTypesMapping(NpgsqlConnection conn)
413         {
414             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "LoadTypesMapping");
415
416             // [TODO] Verify another way to get higher concurrency.
417             lock(typeof(NpgsqlTypesHelper))
418             {
419                 Hashtable oidToNameMapping = (Hashtable) _oidToNameMappings[conn.ServerVersion];
420
421                 if (oidToNameMapping != null)
422                 {
423                     //conn.OidToNameMapping = oidToNameMapping;
424                     return oidToNameMapping;
425                 }
426
427
428                 oidToNameMapping = new Hashtable();
429                 //conn.OidToNameMapping = oidToNameMapping;
430
431                 // Bootstrap value as the datareader below will use ConvertStringToNpgsqlType above.
432                 //oidToNameMapping.Add(26, "oid");
433
434                 NpgsqlCommand command = new NpgsqlCommand("select oid, typname from pg_type where typname in ('bool', 'bytea', 'date', 'float4', 'float8', 'int2', 'int4', 'int8', 'numeric', 'text', 'time', 'timestamp', 'timestamptz', 'timetz');", conn);
435
436                 NpgsqlDataReader dr = command.ExecuteReader();
437
438                 // Data was read. Clear the mapping from previous bootstrap value so we don't get
439                 // exceptions trying to add duplicate key.
440                 // oidToNameMapping.Clear();
441
442                 while (dr.Read())
443                 {
444                     // Add the key as a Int32 value so the switch in ConvertStringToNpgsqlType can use it
445                     // in the search. If don't, the key is added as string and the switch doesn't work.
446
447                     DbType type;
448                     String typeName = (String) dr[1];
449
450                     switch (typeName)
451                     {
452                     case "bool":
453                         type = DbType.Boolean;
454                         break;
455                     case "bytea":
456                         type = DbType.Binary;
457                         break;
458                     case "date":
459                         type = DbType.Date;
460                         break;
461                     case "float4":
462                         type = DbType.Single;
463                         break;
464                     case "float8":
465                         type = DbType.Double;
466                         break;
467                     case "int2":
468                         type = DbType.Int16;
469                         break;
470                     case "int4":
471                         type = DbType.Int32;
472                         break;
473                     case "int8":
474                         type = DbType.Int64;
475                         break;
476                     case "numeric":
477                         type = DbType.Decimal;
478                         break;
479                     case "time":
480                     case "timetz":
481                         type = DbType.Time;
482                         break;
483                     case "timestamp":
484                     case "timestamptz":
485                         type = DbType.DateTime;
486                         break;
487                     default:
488                         type = DbType.String; // Default dbtype of the oid. Unsupported types will be returned as String.
489                         break;
490                     }
491
492
493                     oidToNameMapping.Add(Int32.Parse((String)dr[0]), type);
494                 }
495
496                 _oidToNameMappings.Add(conn.ServerVersion, oidToNameMapping);
497                 return oidToNameMapping;
498             }
499
500
501         }
502
503
504
505         private static Byte[] ConvertByteAToByteArray(String byteA)
506         {
507             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ConvertByteAToByteArray");
508             Int32 octalValue = 0;
509             Int32 byteAPosition = 0;
510
511             Int32 byteAStringLength = byteA.Length;
512
513             MemoryStream ms = new MemoryStream();
514
515             while (byteAPosition < byteAStringLength)
516             {
517
518
519                 // The IsDigit is necessary in case we receive a \ as the octal value and not
520                 // as the indicator of a following octal value in decimal format.
521                 // i.e.: \201\301P\A
522                 if (byteA[byteAPosition] == '\\')
523
524                     if (byteAPosition + 1 == byteAStringLength)
525                     {
526                         octalValue = '\\';
527                         byteAPosition++;
528                     }
529                     else if (Char.IsDigit(byteA[byteAPosition + 1]))
530                     {
531                         octalValue = (Byte.Parse(byteA[byteAPosition + 1].ToString()) << 6);
532                         octalValue |= (Byte.Parse(byteA[byteAPosition + 2].ToString()) << 3);
533                         octalValue |= Byte.Parse(byteA[byteAPosition + 3].ToString());
534                         byteAPosition += 4;
535
536                     }
537                     else
538                     {
539                         octalValue = '\\';
540                         byteAPosition += 2;
541                     }
542
543
544                 else
545                 {
546                     octalValue = (Byte)byteA[byteAPosition];
547                     byteAPosition++;
548                 }
549
550
551                 ms.WriteByte((Byte)octalValue);
552
553             }
554
555             return ms.ToArray();
556
557
558         }
559
560         private static String ConvertByteArrayToBytea(Byte[] byteArray)
561         {
562             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ConvertByteArrayToBytea");
563             int len = byteArray.Length;
564             char[] res = new char[len * 5];
565             for (int i=0, o=0; i<len; ++i, o += 5)
566             {
567                 byte item = byteArray[i];
568                 res[o] = res[o + 1] = '\\';
569                 res[o + 2] = (char)('0' + (7 & (item >> 6)));
570                 res[o + 3] = (char)('0' + (7 & (item >> 3)));
571                 res[o + 4] = (char)('0' + (7 & item));
572             }
573             return new String(res);
574
575         }
576     }
577 }