2004-05-01 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 Exception(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 Exception("Type not supported in binary format");
183             }
184
185
186         }
187
188
189         public static String ConvertNpgsqlParameterToBackendStringValue(NpgsqlParameter parameter)
190         {
191             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ConvertNpgsqlParameterToBackendStringValue");
192
193             if ((parameter.Value == DBNull.Value) || (parameter.Value == null))
194                 return "Null";
195
196             switch(parameter.DbType)
197             {
198             case DbType.Binary:
199                 return "'" + ConvertByteArrayToBytea((Byte[])parameter.Value) + "'";
200             case DbType.Boolean:
201             case DbType.Int64:
202             case DbType.Int32:
203             case DbType.Int16:
204                 return parameter.Value.ToString();
205
206             case DbType.Single:
207                 // To not have a value implicitly converted to float8, we add quotes.
208                 return "'" + ((Single)parameter.Value).ToString(NumberFormatInfo.InvariantInfo) + "'";
209
210             case DbType.Double:
211                 return ((Double)parameter.Value).ToString(NumberFormatInfo.InvariantInfo);
212
213             case DbType.Date:
214                 return "'" + ((DateTime)parameter.Value).ToString("yyyy-MM-dd") + "'";
215
216             case DbType.DateTime:
217                 return "'" + ((DateTime)parameter.Value).ToString("yyyy-MM-dd HH:mm:ss.fff") + "'";
218
219             case DbType.Decimal:
220                 return ((Decimal)parameter.Value).ToString(NumberFormatInfo.InvariantInfo);
221
222             case DbType.String:
223             case DbType.AnsiString:
224             case DbType.StringFixedLength:
225                 return "'" + parameter.Value.ToString().Replace("'", "\\'") + "'";
226
227             case DbType.Time:
228                 return "'" + ((DateTime)parameter.Value).ToString("HH:mm:ss.ffff") + "'";
229
230             default:
231                 // This should not happen!
232                 throw new Exception(String.Format(resman.GetString("Exception_TypeNotSupported"), parameter.DbType));
233
234
235             }
236
237         }
238
239
240         ///<summary>
241         /// This method is responsible to convert the string received from the backend
242         /// to the corresponding NpgsqlType.
243         /// </summary>
244         ///
245         public static Object ConvertBackendStringToSystemType(Hashtable oidToNameMapping, String data, Int32 typeOid, Int32 typeModifier)
246         {
247             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ConvertBackendStringToSystemType");
248             //[TODO] Find a way to eliminate this checking. It is just used at bootstrap time
249             // when connecting because we don't have yet loaded typeMapping. The switch below
250             // crashes with NullPointerReference when it can't find the typeOid.
251
252             if (!oidToNameMapping.ContainsKey(typeOid))
253                 return data;
254
255             switch ((DbType)oidToNameMapping[typeOid])
256             {
257             case DbType.Binary:
258                 return ConvertByteAToByteArray(data);
259
260             case DbType.Boolean:
261                 return (data.ToLower() == "t" ? true : false);
262
263             case DbType.Single:
264                 return Single.Parse(data, NumberFormatInfo.InvariantInfo);
265
266             case DbType.Double:
267                 return Double.Parse(data, NumberFormatInfo.InvariantInfo);
268
269             case DbType.Int16:
270                 return Int16.Parse(data);
271             case DbType.Int32:
272                 return Int32.Parse(data);
273
274             case DbType.Int64:
275                 return Int64.Parse(data);
276
277             case DbType.Decimal:
278                 // Got this manipulation of typemodifier from jdbc driver - file AbstractJdbc1ResultSetMetaData.java.html method getColumnDisplaySize
279                 {
280                     typeModifier -= 4;
281                     //Console.WriteLine("Numeric from server: {0} digitos.digitos {1}.{2}", data, (typeModifier >> 16) & 0xffff, typeModifier & 0xffff);
282                     return Decimal.Parse(data, NumberFormatInfo.InvariantInfo);
283
284                 }
285
286             case DbType.DateTime:
287
288                 // Get the date time parsed in all expected formats for timestamp.
289                 return DateTime.ParseExact(data,
290                                            DateTimeFormats,
291                                            DateTimeFormatInfo.InvariantInfo,
292                                            DateTimeStyles.NoCurrentDateDefault | DateTimeStyles.AllowWhiteSpaces);
293
294             case DbType.Date:
295                 return DateTime.ParseExact(data,
296                                            DateFormats,
297                                            DateTimeFormatInfo.InvariantInfo,
298                                            DateTimeStyles.AllowWhiteSpaces);
299
300             case DbType.Time:
301
302                 return DateTime.ParseExact(data,
303                                            TimeFormats,
304                                            DateTimeFormatInfo.InvariantInfo,
305                                            DateTimeStyles.NoCurrentDateDefault | DateTimeStyles.AllowWhiteSpaces);
306
307             case DbType.String:
308             case DbType.AnsiString:
309             case DbType.StringFixedLength:
310                 return data;
311             default:
312                 throw new Exception(String.Format(resman.GetString("Exception_TypeNotSupported"),  oidToNameMapping[typeOid]));
313
314
315             }
316         }
317
318
319
320         ///<summary>
321         /// This method gets a type oid and return the equivalent
322         /// Npgsql type.
323         /// </summary>
324         ///
325
326         public static Type GetSystemTypeFromTypeOid(Hashtable oidToNameMapping, Int32 typeOid)
327         {
328             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "GetSystemTypeFromTypeOid");
329             // This method gets a db type identifier and return the equivalent
330             // system type.
331
332             //[TODO] Find a way to eliminate this checking. It is just used at bootstrap time
333             // when connecting because we don't have yet loaded typeMapping. The switch below
334             // crashes with NullPointerReference when it can't find the typeOid.
335
336
337
338             if (!oidToNameMapping.ContainsKey(typeOid))
339                 return Type.GetType("System.String");
340
341             switch ((DbType)oidToNameMapping[typeOid])
342             {
343             case DbType.Binary:
344                 return Type.GetType("System.Byte[]");
345             case DbType.Boolean:
346                 return Type.GetType("System.Boolean");
347             case DbType.Int16:
348                 return Type.GetType("System.Int16");
349             case DbType.Single:
350                 return Type.GetType("System.Single");
351             case DbType.Double:
352                 return Type.GetType("System.Double");
353             case DbType.Int32:
354                 return Type.GetType("System.Int32");
355             case DbType.Int64:
356                 return Type.GetType("System.Int64");
357             case DbType.Decimal:
358                 return Type.GetType("System.Decimal");
359             case DbType.DateTime:
360             case DbType.Date:
361             case DbType.Time:
362                 return Type.GetType("System.DateTime");
363             case DbType.String:
364             case DbType.AnsiString:
365             case DbType.StringFixedLength:
366                 return Type.GetType("System.String");
367             default:
368                 throw new Exception(String.Format(resman.GetString("Exception_TypeNotSupported"), oidToNameMapping[typeOid]));
369
370             }
371
372
373         }
374
375
376         ///<summary>
377         /// This method is responsible to send query to get the oid-to-name mapping.
378         /// This is needed as from one version to another, this mapping can be changed and
379         /// so we avoid hardcoding them.
380         /// </summary>
381         public static Hashtable LoadTypesMapping(NpgsqlConnection conn)
382         {
383             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "LoadTypesMapping");
384
385             // [TODO] Verify another way to get higher concurrency.
386             lock(typeof(NpgsqlTypesHelper))
387             {
388                 Hashtable oidToNameMapping = (Hashtable) _oidToNameMappings[conn.ServerVersion];
389
390                 if (oidToNameMapping != null)
391                 {
392                     //conn.OidToNameMapping = oidToNameMapping;
393                     return oidToNameMapping;
394                 }
395
396
397                 oidToNameMapping = new Hashtable();
398                 //conn.OidToNameMapping = oidToNameMapping;
399
400                 // Bootstrap value as the datareader below will use ConvertStringToNpgsqlType above.
401                 //oidToNameMapping.Add(26, "oid");
402
403                 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);
404
405                 NpgsqlDataReader dr = command.ExecuteReader();
406
407                 // Data was read. Clear the mapping from previous bootstrap value so we don't get
408                 // exceptions trying to add duplicate key.
409                 // oidToNameMapping.Clear();
410
411                 while (dr.Read())
412                 {
413                     // Add the key as a Int32 value so the switch in ConvertStringToNpgsqlType can use it
414                     // in the search. If don't, the key is added as string and the switch doesn't work.
415
416                     DbType type;
417                     String typeName = (String) dr[1];
418
419                     switch (typeName)
420                     {
421                     case "bool":
422                         type = DbType.Boolean;
423                         break;
424                     case "bytea":
425                         type = DbType.Binary;
426                         break;
427                     case "date":
428                         type = DbType.Date;
429                         break;
430                     case "float4":
431                         type = DbType.Single;
432                         break;
433                     case "float8":
434                         type = DbType.Double;
435                         break;
436                     case "int2":
437                         type = DbType.Int16;
438                         break;
439                     case "int4":
440                         type = DbType.Int32;
441                         break;
442                     case "int8":
443                         type = DbType.Int64;
444                         break;
445                     case "numeric":
446                         type = DbType.Decimal;
447                         break;
448                     case "time":
449                     case "timetz":
450                         type = DbType.Time;
451                         break;
452                     case "timestamp":
453                     case "timestamptz":
454                         type = DbType.DateTime;
455                         break;
456                     default:
457                         type = DbType.String; // Default dbtype of the oid. Unsupported types will be returned as String.
458                         break;
459                     }
460
461
462                     oidToNameMapping.Add(Int32.Parse((String)dr[0]), type);
463                 }
464
465                 _oidToNameMappings.Add(conn.ServerVersion, oidToNameMapping);
466                 return oidToNameMapping;
467             }
468
469
470         }
471
472
473
474         private static Byte[] ConvertByteAToByteArray(String byteA)
475         {
476             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ConvertByteAToByteArray");
477             Int32 octalValue = 0;
478             Int32 byteAPosition = 0;
479
480             Int32 byteAStringLength = byteA.Length;
481
482             MemoryStream ms = new MemoryStream();
483
484             while (byteAPosition < byteAStringLength)
485             {
486
487
488                 // The IsDigit is necessary in case we receive a \ as the octal value and not
489                 // as the indicator of a following octal value in decimal format.
490                 // i.e.: \201\301P\A
491                 if (byteA[byteAPosition] == '\\')
492
493                     if (byteAPosition + 1 == byteAStringLength)
494                     {
495                         octalValue = '\\';
496                         byteAPosition++;
497                     }
498                     else if (Char.IsDigit(byteA[byteAPosition + 1]))
499                     {
500                         octalValue = (Byte.Parse(byteA[byteAPosition + 1].ToString()) << 6);
501                         octalValue |= (Byte.Parse(byteA[byteAPosition + 2].ToString()) << 3);
502                         octalValue |= Byte.Parse(byteA[byteAPosition + 3].ToString());
503                         byteAPosition += 4;
504
505                     }
506                     else
507                     {
508                         octalValue = '\\';
509                         byteAPosition += 2;
510                     }
511
512
513                 else
514                 {
515                     octalValue = (Byte)byteA[byteAPosition];
516                     byteAPosition++;
517                 }
518
519
520                 ms.WriteByte((Byte)octalValue);
521
522             }
523
524             return ms.ToArray();
525
526
527         }
528
529         private static String ConvertByteArrayToBytea(Byte[] byteArray)
530         {
531             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ConvertByteArrayToBytea");
532             int len = byteArray.Length;
533             char[] res = new char[len * 5];
534             for (int i=0, o=0; i<len; ++i, o += 5)
535             {
536                 byte item = byteArray[i];
537                 res[o] = res[o + 1] = '\\';
538                 res[o + 2] = (char)('0' + (7 & (item >> 6)));
539                 res[o + 3] = (char)('0' + (7 & (item >> 3)));
540                 res[o + 4] = (char)('0' + (7 & item));
541             }
542             return new String(res);
543
544         }
545     }
546 }