2005-01-31 Zoltan Varga <vargaz@freemail.hu>
[mono.git] / mcs / class / Mono.Data.PostgreSqlClient / Mono.Data.PostgreSqlClient / PgSqlConnection.cs
1 //
2 // Mono.Data.PostgreSqlClient.PgSqlConnection.cs
3 //
4 // Author:
5 //   Rodrigo Moya (rodrigo@ximian.com)
6 //   Daniel Morgan (danmorg@sc.rr.com)
7 //   Tim Coleman (tim@timcoleman.com)
8 //
9 // (C) Ximian, Inc 2002
10 // (C) Daniel Morgan 2002
11 // Copyright (C) Tim Coleman, 2002
12 //
13 // Credits:
14 //    SQL and concepts were used from libgda 0.8.190 (GNOME Data Access)\r
15 //    http://www.gnome-db.org/\r
16 //    with permission from the authors of the\r
17 //    PostgreSQL provider in libgda:\r
18 //        Michael Lausch <michael@lausch.at>
19 //        Rodrigo Moya <rodrigo@gnome-db.org>
20 //        Vivien Malerba <malerba@gnome-db.org>
21 //        Gonzalo Paniagua Javier <gonzalo@gnome-db.org>
22 //
23
24 //
25 // Permission is hereby granted, free of charge, to any person obtaining
26 // a copy of this software and associated documentation files (the
27 // "Software"), to deal in the Software without restriction, including
28 // without limitation the rights to use, copy, modify, merge, publish,
29 // distribute, sublicense, and/or sell copies of the Software, and to
30 // permit persons to whom the Software is furnished to do so, subject to
31 // the following conditions:
32 // 
33 // The above copyright notice and this permission notice shall be
34 // included in all copies or substantial portions of the Software.
35 // 
36 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
37 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
38 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
39 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
40 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
41 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
42 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
43 //
44
45 // use #define DEBUG_SqlConnection if you want to spew debug messages
46 // #define DEBUG_SqlConnection
47
48 using System;
49 using System.Collections;
50 using System.Collections.Specialized;
51 using System.ComponentModel;
52 using System.Data;
53 using System.Data.Common;
54 using System.Runtime.InteropServices;
55 using System.Text;
56
57 namespace Mono.Data.PostgreSqlClient {
58
59         /// <summary>
60         /// Represents an open connection to a SQL data source
61         /// </summary>
62         public sealed class PgSqlConnection : Component, IDbConnection,
63                 ICloneable      
64         {
65                 // FIXME: Need to implement class Component, \r
66                 // and interfaces: ICloneable and IDisposable   
67
68                 #region Fields
69
70                 private PostgresTypes types = null;
71                 private IntPtr pgConn = IntPtr.Zero;    
72
73                 // PGConn (Postgres Connection)
74                 private string connectionString = "";    
75                 // OLE DB Connection String
76                 private string pgConnectionString = ""; 
77                 // PostgreSQL Connection String
78                 private PgSqlTransaction trans = null;
79                 private int connectionTimeout = 15;     
80                 // default for 15 seconds
81                 
82                 // connection parameters in connection string
83                 private string host = "";     
84                 // Name of host to connect to
85                 private string hostaddr = ""; 
86                 // IP address of host to connect to
87                 // should be in "n.n.n.n" format
88                 private string port = "";     
89                 // Port number to connect to at the server host
90                 private string dbname = "";   // The database name. 
91                 private string user = "";     // User name to connect as. 
92                 private string password = "";
93                 // Password to be used if the server 
94                 // demands password authentication.             
95                 private string options = ""; 
96                 // Trace/debug options to be sent to the server. 
97                 private string tty = ""; 
98                 // A file or tty for optional 
99                 // debug output from the backend. 
100                 private string requiressl = "";
101                 // Set to 1 to require 
102                 // SSL connection to the backend. 
103                 // Libpq will then refuse to connect 
104                 // if the server does not 
105                 // support SSL. Set to 0 (default) to 
106                 // negotiate with server. 
107
108                 // connection state
109                 private ConnectionState conState = ConnectionState.Closed;
110                 
111                 // DataReader state
112                 private PgSqlDataReader rdr = null;
113                 private bool dataReaderOpen = false;
114                 // FIXME: if true, throw an exception if SqlConnection 
115                 //        is used for anything other than reading
116                 //        data using SqlDataReader
117                 
118                 private string versionString = "Unknown";
119
120                 private bool disposed = false;
121
122                 #endregion // Fields
123
124                 #region Constructors
125
126                 // A lot of the defaults were initialized in the Fields
127                 [MonoTODO]
128                 public PgSqlConnection () {
129
130                 }
131         
132                 [MonoTODO]
133                 public PgSqlConnection (String connectionString) {
134                         SetConnectionString (connectionString);
135                 }
136
137                 #endregion // Constructors
138
139                 #region Destructors
140
141                 protected override void Dispose(bool disposing) {
142                         if(!this.disposed)
143                                 try {
144                                         if(disposing) {
145                                                 // release any managed resources
146                                         }
147                                         // release any unmanaged resources
148                                         // close any handles
149                                                                                 
150                                         this.disposed = true;
151                                 }
152                                 finally {
153                                         base.Dispose(disposing);
154                                 }
155                 }
156         
157                 // aka Finalize()
158                 // [ClassInterface(ClassInterfaceType.AutoDual)]
159                 [MonoTODO]
160                 ~PgSqlConnection() {
161                         Dispose (false);
162                 }
163                 
164                 #endregion // Destructors
165
166                 #region Public Methods
167
168                 IDbTransaction IDbConnection.BeginTransaction () {
169                         return BeginTransaction ();
170                 }
171
172                 public PgSqlTransaction BeginTransaction () {
173                         return TransactionBegin (); // call private method
174                 }
175
176                 IDbTransaction IDbConnection.BeginTransaction (IsolationLevel 
177                         il) {
178                         return BeginTransaction (il);
179                 }
180
181                 public PgSqlTransaction BeginTransaction (IsolationLevel il) {
182                         return TransactionBegin (il); // call private method
183                 }
184
185                 // PostgreSQL does not support named transactions/savepoint
186                 //            nor nested transactions
187                 [Obsolete]
188                 public PgSqlTransaction BeginTransaction(string transactionName) {
189                         return TransactionBegin (); // call private method
190                 }
191
192                 [Obsolete]
193                 public PgSqlTransaction BeginTransaction(IsolationLevel iso,\r
194                         string transactionName) {
195                         return TransactionBegin (iso); // call private method
196                 }
197
198                 [MonoTODO]
199                 public void ChangeDatabase (string databaseName) {
200                         throw new NotImplementedException ();
201                 }
202
203                 object ICloneable.Clone() {
204                         throw new NotImplementedException ();
205                 }
206                 
207                 [MonoTODO]
208                 public void Close () {
209                         if(dataReaderOpen == true) {
210                                 // TODO: what do I do if
211                                 // the user Closes the connection
212                                 // without closing the Reader first?
213
214                         }                       
215                         CloseDataSource ();
216                 }
217
218                 IDbCommand IDbConnection.CreateCommand () {
219                         return CreateCommand ();
220                 }
221
222                 public PgSqlCommand CreateCommand () {
223                         PgSqlCommand sqlcmd = new PgSqlCommand ("", this);
224
225                         return sqlcmd;
226                 }
227
228                 [MonoTODO]
229                 public void Open () {
230                         if(dbname.Equals(""))
231                                 throw new InvalidOperationException(
232                                         "dbname missing");
233                         else if(conState == ConnectionState.Open)
234                                 throw new InvalidOperationException(
235                                         "ConnnectionState is already Open");
236
237                         ConnStatusType connStatus;
238
239                         // FIXME: check to make sure we have 
240                         //        everything to connect,
241                         //        otherwise, throw an exception
242
243                         pgConn = PostgresLibrary.PQconnectdb 
244                                 (pgConnectionString);
245
246                         // FIXME: should we use PQconnectStart/PQconnectPoll
247                         //        instead of PQconnectdb?  
248                         // PQconnectdb blocks 
249                         // PQconnectStart/PQconnectPoll is non-blocking
250                         
251                         connStatus = PostgresLibrary.PQstatus (pgConn);
252                         if(connStatus == ConnStatusType.CONNECTION_OK) {
253                                 // Successfully Connected
254                                 disposed = false;
255
256                                 SetupConnection();
257                         }
258                         else {
259                                 String errorMessage = PostgresLibrary.
260                                         PQerrorMessage (pgConn);
261                                 errorMessage += ": Could not connect to database.";
262
263                                 throw new PgSqlException(0, 0,
264                                         errorMessage, 0, "",
265                                         host, "SqlConnection", 0);
266                         }
267                 }
268
269                 #endregion // Public Methods
270
271                 #region Internal Methods
272
273                 // Used to prevent SqlConnection
274                 // from doing anything while
275                 // SqlDataReader is open.
276                 // Open the Reader. (called from SqlCommand)
277                 internal void OpenReader(PgSqlDataReader reader) 
278                 {       
279                         if(dataReaderOpen == true) {
280                                 // TODO: throw exception here?
281                                 //       because a reader
282                                 //       is already open
283                         }
284                         else {
285                                 rdr = reader;
286                                 dataReaderOpen = true;
287                         }
288                 }
289
290                 // Used to prevent SqlConnection
291                 // from doing anything while
292                 // SqlDataReader is open
293                 // Close the Reader (called from SqlCommand)
294                 // if closeConnection true, Close() the connection
295                 // this is based on CommandBehavior.CloseConnection
296                 internal void CloseReader(bool closeConnection)
297                 {       if(closeConnection == true)
298                                 CloseDataSource();
299                         else
300                                 dataReaderOpen = false;
301                 }
302
303                 #endregion // Internal Methods
304
305                 #region Private Methods
306
307                 void SetupConnection() {
308                         
309                         conState = ConnectionState.Open;
310
311                         // FIXME: load types into hashtable
312                         types = new PostgresTypes(this);
313                         types.Load();
314
315                         versionString = GetDatabaseServerVersion();
316
317                         // set DATE style to YYYY/MM/DD
318                         IntPtr pgResult = IntPtr.Zero;
319                         pgResult = PostgresLibrary.PQexec (pgConn, "SET DATESTYLE TO 'ISO'");
320                         PostgresLibrary.PQclear (pgResult);
321                         pgResult = IntPtr.Zero;
322                 }
323
324                 string GetDatabaseServerVersion() 
325                 {
326                         PgSqlCommand cmd = new PgSqlCommand("select version()",this);
327                         return (string) cmd.ExecuteScalar();
328                 }
329
330                 void CloseDataSource () {
331                         // FIXME: just a quick hack
332                         if(conState == ConnectionState.Open) {
333                                 if(trans != null)
334                                         if(trans.DoingTransaction == true) {
335                                                 trans.Rollback();
336                                                 // trans.Dispose();
337                                                 trans = null;
338                                         }
339
340                                 conState = ConnectionState.Closed;
341                                 PostgresLibrary.PQfinish (pgConn);
342                                 pgConn = IntPtr.Zero;
343                         }
344                 }
345
346                 void SetConnectionString (string connectionString) {
347                         this.connectionString = connectionString;
348                         StringBuilder postgresConnectionString = new StringBuilder ();
349
350                         connectionString += ";";
351                         NameValueCollection parameters = new NameValueCollection ();
352
353                         if (connectionString == String.Empty)
354                                 return;
355
356                         bool inQuote = false;
357                         bool inDQuote = false;
358
359                         string name = String.Empty;
360                         string value = String.Empty;
361                         StringBuilder sb = new StringBuilder ();
362
363                         foreach (char c in connectionString) {
364                                 switch (c) {
365                                 case '\'':
366                                         inQuote = !inQuote;
367                                         break;
368                                 case '"' :
369                                         inDQuote = !inDQuote;
370                                         break;
371                                 case ';' :
372                                         if (!inDQuote && !inQuote) {
373                                                 if (name != String.Empty && name != null) {
374                                                         value = sb.ToString ();
375                                                         parameters [name.ToUpper ().Trim ()] = value.Trim ();
376                                                 }
377                                                 name = String.Empty;
378                                                 value = String.Empty;
379                                                 sb = new StringBuilder ();
380                                         }
381                                         else
382                                                 sb.Append (c);
383                                         break;
384                                 case '=' :
385                                         if (!inDQuote && !inQuote) {
386                                                 name = sb.ToString ();
387                                                 sb = new StringBuilder ();
388                                         }
389                                         else
390                                                 sb.Append (c);
391                                         break;
392                                 default:
393                                         sb.Append (c);
394                                         break;
395                                 }
396                         }
397
398                         SetProperties (parameters);
399                 }
400
401                 private void SetProperties (NameValueCollection parameters) {
402                         StringBuilder postgresConnectionString = new StringBuilder ();
403
404                         string value;
405                         foreach (string name in parameters) {
406                                 value = parameters[name];
407
408                                 bool found = true;
409                                 switch (name) {
410                                 case "PORT" :
411                                         port = value;
412                                         break;
413                                 case "DATA SOURCE" :
414                                 case "SERVER" :
415                                 case "HOST" :
416                                         // set DataSource property
417                                         host = value;
418                                         break;
419                                 case "OPTIONS" :
420                                         options = value;
421                                         break;
422                                 case "TTY" :
423                                         tty = value;
424                                         break;          
425                                 case "REQUIRESSL" :
426                                         requiressl = value;
427                                         break;
428                                 case "ADDRESS" :
429                                 case "ADDR" :
430                                 case "NETWORK ADDRESS" :
431                                 case "HOSTADDR" :
432                                         hostaddr = value;
433                                         break;
434                                 case "INITIAL CATALOG" :
435                                 case "DATABASE" :
436                                 case "DBNAME":
437                                         // set Database property
438                                         dbname = value;
439                                         break;
440                                 case "PASSWORD" :
441                                 case "PWD" :
442                                         password = value;
443                                         break;
444                                 case "UID" :
445                                 case "USER ID" :
446                                 case "USER" :
447                                         user = value;
448                                         break;
449                                 default:
450                                         found = false;
451                                         break;
452                                 }
453                                 if (found == true) {
454                                         string valuePair = name.ToLower() + "=" + value;
455                                         postgresConnectionString.Append (valuePair + " ");
456                                 }
457                         }
458                         this.pgConnectionString = postgresConnectionString.ToString ();
459                 }
460
461                 private PgSqlTransaction TransactionBegin () {
462                         // FIXME: need to keep track of 
463                         // transaction in-progress
464                         trans = new PgSqlTransaction ();
465                         // using internal methods of SqlTransaction
466                         trans.SetConnection (this);
467                         trans.Begin();
468
469                         return trans;
470                 }
471
472                 private PgSqlTransaction TransactionBegin (IsolationLevel il) {
473                         // FIXME: need to keep track of 
474                         // transaction in-progress
475                         trans = new PgSqlTransaction ();
476                         // using internal methods of SqlTransaction
477                         trans.SetConnection (this);
478                         trans.SetIsolationLevel (il);
479                         trans.Begin();
480                         
481                         return trans;
482                 }
483
484                 #endregion
485
486                 #region Public Properties
487
488                 [MonoTODO]
489                 public ConnectionState State            {
490                         get { 
491                                 return conState;
492                         }
493                 }
494
495                 public string ConnectionString  {
496                         get { 
497                                 return connectionString;
498                         }
499                         set { 
500                                 SetConnectionString (value);
501                         }
502                 }
503                 
504                 public int ConnectionTimeout {
505                         get { 
506                                 return connectionTimeout; 
507                         }
508                 }
509
510                 public string Database  {
511                         get { 
512                                 return dbname; 
513                         }
514                 }
515
516                 public string DataSource {
517                         get {
518                                 return host;
519                         }
520                 }
521
522                 public int PacketSize {
523                         get { 
524                                 throw new NotImplementedException ();
525                         }
526                 }
527
528                 public string ServerVersion {
529                         get { 
530                                 return versionString;
531                         }
532                 }
533
534                 #endregion // Public Properties
535
536                 #region Internal Properties
537
538                 // For System.Data.SqlClient classes
539                 // to get the current transaction
540                 // in progress - if any
541                 internal PgSqlTransaction Transaction {
542                         get {
543                                 return trans;
544                         }
545                 }
546
547                 // For System.Data.SqlClient classes 
548                 // to get the unmanaged PostgreSQL connection
549                 internal IntPtr PostgresConnection {
550                         get {
551                                 return pgConn;
552                         }
553                 }
554
555                 // For System.Data.SqlClient classes
556                 // to get the list PostgreSQL types
557                 // so can look up based on OID to
558                 // get the .NET System type.
559                 internal ArrayList Types {
560                         get {
561                                 return types.List;
562                         }
563                 }
564
565                 // Used to prevent SqlConnection
566                 // from doing anything while
567                 // SqlDataReader is open
568                 internal bool IsReaderOpen {
569                         get {
570                                 return dataReaderOpen;
571                         }
572                 }
573
574                 #endregion // Internal Properties
575
576                 #region Events
577                 
578                 public event 
579                 PgSqlInfoMessageEventHandler InfoMessage;
580
581                 public event 
582                 StateChangeEventHandler StateChange;
583                 
584                 #endregion
585         
586                 #region Inner Classes
587
588                 private class PostgresTypes {
589                         // TODO: create hashtable for 
590                         // PostgreSQL types to .NET types
591                         // containing: oid, typname, SqlDbType
592
593                         private Hashtable hashTypes;
594                         private ArrayList pgTypes;
595                         private PgSqlConnection con;
596
597                         // Got this SQL with the permission from 
598                         // the authors of libgda
599                         private const string SEL_SQL_GetTypes = 
600                                 "SELECT oid, typname FROM pg_type " +
601                                 "WHERE typrelid = 0 AND typname !~ '^_' " +
602                                 " AND  typname not in ('SET', 'cid', " +
603                                 "'int2vector', 'oidvector', 'regproc', " +
604                                 "'smgr', 'tid', 'unknown', 'xid') " +
605                                 "ORDER BY typname";
606
607                         internal PostgresTypes(PgSqlConnection sqlcon) {
608                                 
609                                 con = sqlcon;
610                                 hashTypes = new Hashtable();
611                         }
612
613                         private void AddPgType(Hashtable types, \r
614                                 string typname, DbType dbType) {\r
615 \r
616                                 PostgresType pgType = new PostgresType();\r
617                         \r
618                                 pgType.typname = typname;\r
619                                 pgType.dbType = dbType; \r
620 \r
621                                 types.Add(pgType.typname, pgType);\r
622                         }\r
623
624                         private void BuildTypes(IntPtr pgResult, 
625                                 int nRows, int nFields) {
626
627                                 String value;
628
629                                 int r;
630                                 for(r = 0; r < nRows; r++) {
631                                         PostgresType pgType = 
632                                                 new PostgresType();
633
634                                         // get data value (oid)
635                                         value = PostgresLibrary.
636                                                 PQgetvalue(
637                                                         pgResult,
638                                                         r, 0);
639                                                 
640                                         pgType.oid = Int32.Parse(value);\r
641 \r
642                                         // get data value (typname)\r
643                                         value = PostgresLibrary.
644                                                 PQgetvalue(
645                                                 pgResult,
646                                                 r, 1);  \r
647                                         pgType.typname = String.Copy(value);\r
648                                         pgType.dbType = PostgresHelper.\r
649                                                         TypnameToSqlDbType(\r
650                                                                 pgType.typname);\r
651 \r
652                                         pgTypes.Add(pgType);
653                                 }
654                                 pgTypes = ArrayList.ReadOnly(pgTypes);
655                         }
656
657                         internal void Load() {
658                                 pgTypes = new ArrayList();
659                                 IntPtr pgResult = IntPtr.Zero; // PGresult
660                                 
661                                 if(con.State != ConnectionState.Open)
662                                         throw new InvalidOperationException(
663                                                 "ConnnectionState is not Open");
664
665                                 // FIXME: PQexec blocks 
666                                 // while PQsendQuery is non-blocking
667                                 // which is better to use?
668                                 // int PQsendQuery(PGconn *conn,
669                                 //        const char *query);
670
671                                 // execute SQL command
672                                 // uses internal property to get the PGConn IntPtr
673                                 pgResult = PostgresLibrary.
674                                         PQexec (con.PostgresConnection, SEL_SQL_GetTypes);
675
676                                 if(pgResult.Equals(IntPtr.Zero)) {
677                                         throw new PgSqlException(0, 0,
678                                                 "No Resultset from PostgreSQL", 0, "",
679                                                 con.DataSource, "SqlConnection", 0);
680                                 }
681                                 else {
682                                         ExecStatusType execStatus;
683
684                                         execStatus = PostgresLibrary.
685                                                 PQresultStatus (pgResult);
686                         
687                                         if(execStatus == ExecStatusType.PGRES_TUPLES_OK) {
688                                                 int nRows;
689                                                 int nFields;
690
691                                                 nRows = PostgresLibrary.
692                                                         PQntuples(pgResult);
693
694                                                 nFields = PostgresLibrary.
695                                                         PQnfields(pgResult);
696
697                                                 BuildTypes (pgResult, nRows, nFields);
698
699                                                 // close result set
700                                                 PostgresLibrary.PQclear (pgResult);
701                                                 pgResult = IntPtr.Zero;
702                                         }
703                                         else {
704                                                 String errorMessage;
705                                 
706                                                 errorMessage = PostgresLibrary.
707                                                         PQresStatus(execStatus);
708
709                                                 errorMessage += " " + PostgresLibrary.\r
710                                                         PQresultErrorMessage(pgResult);\r
711 \r
712                                                 // close result set
713                                                 PostgresLibrary.PQclear (pgResult);
714                                                 pgResult = IntPtr.Zero;
715 \r
716                                                 throw new PgSqlException(0, 0,
717                                                         errorMessage, 0, "",
718                                                         con.DataSource, "SqlConnection", 0);\r
719                                         }
720                                 }
721                         }
722
723                         public ArrayList List {
724                                 get {
725                                         return pgTypes;
726                                 }
727                         }
728                 }
729
730                 #endregion
731         }
732 }