MySql - fixed problem where socket was not getting closed properly (thanks Steve!)
[mono.git] / mcs / class / ByteFX.Data / mysqlclient / datareader.cs
1 // ByteFX.Data data access components for .Net
2 // Copyright (C) 2002-2003  ByteFX, Inc.
3 //
4 // This library is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU Lesser General Public
6 // License as published by the Free Software Foundation; either
7 // version 2.1 of the License, or (at your option) any later version.
8 // 
9 // This library is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 // Lesser General Public License for more details.
13 // 
14 // You should have received a copy of the GNU Lesser General Public
15 // License along with this library; if not, write to the Free Software
16 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18 using System;
19 using System.Data;
20 using System.Collections;
21
22 namespace ByteFX.Data.MySqlClient
23 {
24         public sealed class MySqlDataReader : MarshalByRefObject, IEnumerable, IDataReader, IDisposable, IDataRecord
25         {
26                 // The DataReader should always be open when returned to the user.
27                 private bool                    isOpen = true;
28
29                 // Keep track of the results and position
30                 // within the resultset (starts prior to first record).
31                 private MySqlField[]    _fields;
32                 private CommandBehavior commandBehavior;
33                 private MySqlCommand    command;
34                 private bool                    canRead;
35                 private bool                    hasRows;
36 //              private Packet                  rowPacket = null;
37
38                 /* 
39                  * Keep track of the connection in order to implement the
40                  * CommandBehavior.CloseConnection flag. A null reference means
41                  * normal behavior (do not automatically close).
42                  */
43                 private MySqlConnection connection = null;
44
45                 /*
46                  * Because the user should not be able to directly create a 
47                  * DataReader object, the constructors are
48                  * marked as internal.
49                  */
50                 internal MySqlDataReader( MySqlCommand cmd, CommandBehavior behavior)
51                 {
52                         this.command = cmd;
53                         connection = (MySqlConnection)command.Connection;
54                         commandBehavior = behavior;
55                 }
56
57                 /****
58                  * METHODS / PROPERTIES FROM IDataReader.
59                  ****/
60                 public int Depth 
61                 {
62                         /*
63                          * Always return a value of zero if nesting is not supported.
64                          */
65                         get { return 0;  }
66                 }
67
68                 /// <summary>
69                 /// Gets a value indicating whether the data reader is closed.
70                 /// </summary>
71                 public bool IsClosed
72                 {
73                         get  { return ! isOpen; }
74                 }
75
76                 public void Dispose() 
77                 {
78                         if (isOpen)
79                                 Close();
80                 }
81
82                 /// <summary>
83                 /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement.
84                 /// </summary>
85                 public int RecordsAffected 
86                 {
87                         // RecordsAffected returns the number of rows affected in batch
88                         // statments from insert/delete/update statments.  This property
89                         // is not completely accurate until .Close() has been called.
90                         get { return command.UpdateCount; }
91                 }
92
93                 /// <summary>
94                 /// Gets a value indicating whether the MySqlDataReader contains one or more rows.
95                 /// </summary>
96                 public bool HasRows
97                 {
98                         get { return hasRows; }
99                 }
100
101                 /// <summary>
102                 /// Closes the MySqlDataReader object.
103                 /// </summary>
104                 public void Close()
105                 {
106                         // finish any current command
107                         ClearCurrentResult();
108                         command.ExecuteBatch(false);
109                         connection.InternalConnection.Driver.ClearPeekedPacket();
110
111                         connection.Reader = null;
112                         if (0 != (commandBehavior & CommandBehavior.CloseConnection))
113                                 connection.Close();
114
115                         isOpen = false;
116                 }
117
118
119
120
121                 /// <summary>
122                 /// Gets the number of columns in the current row.
123                 /// </summary>
124                 public int FieldCount
125                 {
126                         // Return the count of the number of columns, which in
127                         // this case is the size of the column metadata
128                         // array.
129                         get 
130                         { 
131                                 if (_fields != null)
132                                         return _fields.Length;
133                                 return 0;
134                         }
135                 }
136
137                 /// <summary>
138                 /// Overloaded. Gets the value of a column in its native format.
139                 /// In C#, this property is the indexer for the MySqlDataReader class.
140                 /// </summary>
141                 public object this [ int i ]
142                 {
143                         get 
144                         {
145                                 return this.GetValue(i);
146                         }
147                 }
148
149                 public object this [ String name ]
150                 {
151                         // Look up the ordinal and return 
152                         // the value at that position.
153                         get { return this[GetOrdinal(name)]; }
154                 }
155
156                 private MySqlField GetField(int i)
157                 {
158                         if (i >= _fields.Length) throw new IndexOutOfRangeException();
159                         return _fields[i];
160                 }
161
162                 #region TypeSafe Accessors
163                 /// <summary>
164                 /// Gets the value of the specified column as a Boolean.
165                 /// </summary>
166                 /// <param name="i"></param>
167                 /// <returns></returns>
168                 public bool GetBoolean(int i)
169                 {
170                         return Convert.ToBoolean(GetValue(i));
171                 }
172
173                 /// <summary>
174                 /// Gets the value of the specified column as a byte.
175                 /// </summary>
176                 /// <param name="i"></param>
177                 /// <returns></returns>
178                 public byte GetByte(int i)
179                 {
180                         return Convert.ToByte(GetValue(i));
181                 }
182
183                 /// <summary>
184                 /// Reads a stream of bytes from the specified column offset into the buffer an array starting at the given buffer offset.
185                 /// </summary>
186                 /// <param name="i"></param>
187                 /// <param name="fieldOffset"></param>
188                 /// <param name="buffer"></param>
189                 /// <param name="bufferoffset"></param>
190                 /// <param name="length"></param>
191                 /// <returns></returns>
192                 public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length)
193                 {
194                         if (i >= _fields.Length) 
195                                 throw new IndexOutOfRangeException();
196
197                         byte[] bytes = (byte[])GetValue(i);
198
199                         if (buffer == null) 
200                                 return bytes.Length;
201
202                         /// adjust the length so we don't run off the end
203                         if (bytes.Length < (fieldOffset+length)) 
204                         {
205                                 length = (int)(bytes.Length - fieldOffset);
206                         }
207
208                         for (int x=0; x < length; x++)
209                         {
210                                 buffer[bufferoffset+x] = bytes[fieldOffset+x];  
211                         }
212
213                         return length;
214                 }
215
216                 /// <summary>
217                 /// Gets the value of the specified column as a single character.
218                 /// </summary>
219                 /// <param name="i"></param>
220                 /// <returns></returns>
221                 public char GetChar(int i)
222                 {
223                         return Convert.ToChar(GetValue(i));
224                 }
225
226                 /// <summary>
227                 /// Reads a stream of characters from the specified column offset into the buffer as an array starting at the given buffer offset.
228                 /// </summary>
229                 /// <param name="i"></param>
230                 /// <param name="fieldOffset"></param>
231                 /// <param name="buffer"></param>
232                 /// <param name="bufferoffset"></param>
233                 /// <param name="length"></param>
234                 /// <returns></returns>
235                 public long GetChars(int i, long fieldOffset, char[] buffer, int bufferoffset, int length)
236                 {
237                         if (i >= _fields.Length) 
238                                 throw new IndexOutOfRangeException();
239
240                         // retrieve the bytes of the column
241                         long bytesize = GetBytes(i, 0, null, 0, 0);
242                         byte[] bytes = new byte[bytesize];
243                         GetBytes(i, 0, bytes, 0, (int)bytesize);
244
245                         char[] chars = System.Text.Encoding.UTF8.GetChars(bytes, 0, (int)bytesize);
246
247                         if (buffer == null) 
248                                 return chars.Length;
249
250                         /// adjust the length so we don't run off the end
251                         if (chars.Length < (fieldOffset+length)) 
252                         {
253                                 length = (int)(chars.Length - fieldOffset);
254                         }
255
256                         for (int x=0; x < length; x++)
257                         {
258                                 buffer[bufferoffset+x] = chars[fieldOffset+x];  
259                         }
260
261                         return length;
262                 }
263
264                 /// <summary>
265                 /// Gets the name of the source data type.
266                 /// </summary>
267                 /// <param name="i"></param>
268                 /// <returns></returns>
269                 public String GetDataTypeName(int i)
270                 {
271                         if (! isOpen) throw new Exception("No current query in data reader");
272                         if (i >= _fields.Length) throw new IndexOutOfRangeException();
273
274                         // return the name of the type used on the backend
275                         return _fields[i].GetFieldTypeName();
276                 }
277
278                 /// <summary>
279                 /// Gets the value of the specified column as a DateTime object.
280                 /// </summary>
281                 /// <param name="i"></param>
282                 /// <returns></returns>
283                 public DateTime GetDateTime(int i)
284                 {
285                         return Convert.ToDateTime(GetValue(i));
286                 }
287
288                 /// <summary>
289                 /// Gets the value of the specified column as a Decimal object.
290                 /// </summary>
291                 /// <param name="i"></param>
292                 /// <returns></returns>
293                 public Decimal GetDecimal(int i)
294                 {
295                         return Convert.ToDecimal(GetValue(i));
296                 }
297
298                 /// <summary>
299                 /// Gets the value of the specified column as a double-precision floating point number.
300                 /// </summary>
301                 /// <param name="i"></param>
302                 /// <returns></returns>
303                 public double GetDouble(int i)
304                 {
305                         return Convert.ToDouble(GetValue(i));
306                 }
307
308                 /// <summary>
309                 /// Gets the Type that is the data type of the object.
310                 /// </summary>
311                 /// <param name="i"></param>
312                 /// <returns></returns>
313                 public Type GetFieldType(int i)
314                 {
315                         if (! isOpen) throw new Exception("No current query in data reader");
316                         if (i >= _fields.Length) throw new IndexOutOfRangeException();
317
318                         return _fields[i].GetFieldType();
319                 }
320
321                 /// <summary>
322                 /// Gets the value of the specified column as a single-precision floating point number.
323                 /// </summary>
324                 /// <param name="i"></param>
325                 /// <returns></returns>
326                 public float GetFloat(int i)
327                 {
328                         return Convert.ToSingle(GetValue(i));
329                 }
330
331                 /// <summary>
332                 /// Gets the value of the specified column as a globally-unique identifier (GUID).
333                 /// This is currently not supported.
334                 /// </summary>
335                 /// <param name="i"></param>
336                 /// <returns></returns>
337                 public Guid GetGuid(int i)
338                 {
339                         /*
340                         * Force the cast to return the type. InvalidCastException
341                         * should be thrown if the data is not already of the correct type.
342                         */
343                         // The sample does not support this method.
344                         throw new NotSupportedException("GetGUID not supported.");
345                 }
346
347                 /// <summary>
348                 /// Gets the value of the specified column as a 16-bit signed integer.
349                 /// </summary>
350                 /// <param name="i"></param>
351                 /// <returns></returns>
352                 public Int16 GetInt16(int i)
353                 {
354                         return Convert.ToInt16(GetValue(i));
355                 }
356
357                 /// <summary>
358                 /// Gets the value of the specified column as a 32-bit signed integer.
359                 /// </summary>
360                 /// <param name="i"></param>
361                 /// <returns></returns>
362                 public Int32 GetInt32(int i)
363                 {
364                         return Convert.ToInt32(GetValue(i));
365                 }
366
367                 /// <summary>
368                 /// Gets the value of the specified column as a 64-bit signed integer.
369                 /// </summary>
370                 /// <param name="i"></param>
371                 /// <returns></returns>
372                 public Int64 GetInt64(int i)
373                 {
374                         return Convert.ToInt64(GetValue(i));
375                 }
376
377                 /// <summary>
378                 /// Gets the name of the specified column.
379                 /// </summary>
380                 /// <param name="i"></param>
381                 /// <returns></returns>
382                 public String GetName(int i)
383                 {
384                         return _fields[i].ColumnName;
385                 }
386
387                 /// <summary>
388                 /// Gets the column ordinal, given the name of the column.
389                 /// </summary>
390                 /// <param name="name"></param>
391                 /// <returns></returns>
392                 public int GetOrdinal(string name)
393                 {
394                         if (! isOpen)
395                                 throw new Exception("No current query in data reader");
396
397                         for (int i=0; i < _fields.Length; i ++) 
398                         {
399                                 if (_fields[i].ColumnName.ToLower().Equals(name.ToLower()))
400                                         return i;
401                         }
402
403                         // Throw an exception if the ordinal cannot be found.
404                         throw new IndexOutOfRangeException("Could not find specified column in results");
405                 }
406
407                 /// <summary>
408                 /// Returns a DataTable that describes the column metadata of the MySqlDataReader.
409                 /// </summary>
410                 /// <returns></returns>
411                 public DataTable GetSchemaTable()
412                 {
413                         // Only Results from SQL SELECT Queries 
414                         // get a DataTable for schema of the result
415                         // otherwise, DataTable is null reference
416                         if (_fields.Length == 0) return null;
417
418                         DataTable dataTableSchema = new DataTable ("SchemaTable");
419                         
420                         dataTableSchema.Columns.Add ("ColumnName", typeof (string));
421                         dataTableSchema.Columns.Add ("ColumnOrdinal", typeof (int));
422                         dataTableSchema.Columns.Add ("ColumnSize", typeof (int));
423                         dataTableSchema.Columns.Add ("NumericPrecision", typeof (int));
424                         dataTableSchema.Columns.Add ("NumericScale", typeof (int));
425                         dataTableSchema.Columns.Add ("IsUnique", typeof (bool));
426                         dataTableSchema.Columns.Add ("IsKey", typeof (bool));
427                         DataColumn dc = dataTableSchema.Columns["IsKey"];
428                         dc.AllowDBNull = true; // IsKey can have a DBNull
429                         dataTableSchema.Columns.Add ("BaseCatalogName", typeof (string));
430                         dataTableSchema.Columns.Add ("BaseColumnName", typeof (string));
431                         dataTableSchema.Columns.Add ("BaseSchemaName", typeof (string));
432                         dataTableSchema.Columns.Add ("BaseTableName", typeof (string));
433                         dataTableSchema.Columns.Add ("DataType", typeof(Type));
434                         dataTableSchema.Columns.Add ("AllowDBNull", typeof (bool));
435                         dataTableSchema.Columns.Add ("ProviderType", typeof (int));
436                         dataTableSchema.Columns.Add ("IsAliased", typeof (bool));
437                         dataTableSchema.Columns.Add ("IsExpression", typeof (bool));
438                         dataTableSchema.Columns.Add ("IsIdentity", typeof (bool));
439                         dataTableSchema.Columns.Add ("IsAutoIncrement", typeof (bool));
440                         dataTableSchema.Columns.Add ("IsRowVersion", typeof (bool));
441                         dataTableSchema.Columns.Add ("IsHidden", typeof (bool));
442                         dataTableSchema.Columns.Add ("IsLong", typeof (bool));
443                         dataTableSchema.Columns.Add ("IsReadOnly", typeof (bool));
444
445                         int ord = 1;
446                         foreach (MySqlField f in _fields)
447                         {
448                                 DataRow r = dataTableSchema.NewRow();
449                                 r["ColumnName"] = f.ColumnName;
450                                 r["ColumnOrdinal"] = ord++;
451                                 r["ColumnSize"] = f.ColumnLength;
452                                 int prec = f.NumericPrecision();
453                                 int pscale = f.NumericScale();
454                                 if (prec != -1)
455                                         r["NumericPrecision"] = (short)prec;
456                                 if (pscale != -1)
457                                         r["NumericScale"] = (short)pscale;
458                                 r["DataType"] = f.GetFieldType();
459                                 r["ProviderType"] = (int)f.GetMySqlDbType();
460                                 r["IsLong"] = f.IsBlob() && f.ColumnLength > 255;
461                                 r["AllowDBNull"] = f.AllowsNull();
462                                 r["IsReadOnly"] = false;
463                                 r["IsRowVersion"] = false;
464                                 r["IsUnique"] = f.IsUnique();
465                                 r["IsKey"] = f.IsPrimaryKey();
466                                 r["IsAutoIncrement"] = f.IsAutoIncrement();
467                                 r["BaseSchemaName"] = null;
468                                 r["BaseCatalogName"] = null;
469                                 r["BaseTableName"] = f.TableName;
470                                 r["BaseColumnName"] = f.ColumnName;
471
472                                 dataTableSchema.Rows.Add( r );
473                         }
474
475                         return dataTableSchema;
476                 }
477
478                 /// <summary>
479                 /// Gets the value of the specified column as a string.
480                 /// </summary>
481                 /// <param name="i"></param>
482                 /// <returns></returns>
483                 public String GetString(int i)
484                 {
485                         return GetValue(i).ToString();
486                 }
487
488                 /// <summary>
489                 /// Gets the value of the specified column in its native format.
490                 /// </summary>
491                 /// <param name="i"></param>
492                 /// <returns></returns>
493                 public object GetValue(int i)
494                 {
495                         if (! isOpen) throw new Exception("No current query in data reader");
496                         if (i >= _fields.Length) throw new IndexOutOfRangeException();
497
498                         return _fields[i].GetValue();
499                 }
500
501                 /// <summary>
502                 /// Gets all attribute columns in the collection for the current row.
503                 /// </summary>
504                 /// <param name="values"></param>
505                 /// <returns></returns>
506                 public int GetValues(object[] values)
507                 {
508                         for (int i=0; i < _fields.Length; i ++) 
509                         {
510                                 values[i] = GetValue(i);
511                         }
512
513                         return 0;
514                 }
515
516
517                 /// <summary>
518                 /// Gets the value of the specified column as a 16-bit unsigned integer.
519                 /// </summary>
520                 /// <param name="i"></param>
521                 /// <returns></returns>
522                 public UInt16 GetUInt16( int i )
523                 {
524                         return Convert.ToUInt16(GetValue(i));
525                 }
526
527                 /// <summary>
528                 /// Gets the value of the specified column as a 32-bit unsigned integer.
529                 /// </summary>
530                 /// <param name="i"></param>
531                 /// <returns></returns>
532                 public UInt32 GetUInt32( int i )
533                 {
534                         return Convert.ToUInt32(GetValue(i));
535                 }
536
537                 /// <summary>
538                 /// Gets the value of the specified column as a 64-bit unsigned integer.
539                 /// </summary>
540                 /// <param name="i"></param>
541                 /// <returns></returns>
542                 public UInt64 GetUInt64( int i )
543                 {
544                         return Convert.ToUInt64(GetValue(i));
545                 }
546
547
548                 #endregion
549
550                 public IDataReader GetData(int i)
551                 {
552                         /*
553                         * The sample code does not support this method. Normally,
554                         * this would be used to expose nested tables and
555                         * other hierarchical data.
556                         */
557                         throw new NotSupportedException("GetData not supported.");
558                 }
559
560                 /// <summary>
561                 /// Gets a value indicating whether the column contains non-existent or missing values.
562                 /// </summary>
563                 /// <param name="i"></param>
564                 /// <returns></returns>
565                 public bool IsDBNull(int i)
566                 {
567                         return DBNull.Value == GetValue(i);
568                 }
569
570                 /// <summary>
571                 /// Advances the data reader to the next result, when reading the results of batch SQL statements.
572                 /// </summary>
573                 /// <returns></returns>
574                 public bool NextResult()
575                 {
576                         if (! isOpen)
577                                 throw new MySqlException("Invalid attempt to NextResult when reader is closed.");
578
579                         Driver driver = connection.InternalConnection.Driver;
580
581                         ClearCurrentResult();
582
583                         // tell our command to execute the next sql batch
584                         Packet packet = command.ExecuteBatch(true);
585
586                         // if there was no more batches, then signal done
587                         if (packet == null) return false;
588
589                         // When executing query statements, the result byte that is returned
590                         // from MySql is the column count.  That is why we reference the LastResult
591                         // property here to dimension our field array
592                         connection.SetState( ConnectionState.Fetching );
593                         
594                         _fields = new MySqlField[ packet.ReadLenInteger() ];
595                         for (int x=0; x < _fields.Length; x++) 
596                         {
597                                 _fields[x] = new MySqlField();
598                                 _fields[x].ReadSchemaInfo( packet );
599                         }
600
601                         // now take a quick peek at the next packet to see if we have rows
602                         // 
603                         packet = driver.PeekPacket();
604                         hasRows = packet.Type != PacketType.Last;
605                         canRead = hasRows;
606
607                         connection.SetState( ConnectionState.Open );
608                         return true;
609                 }
610
611                 /// <summary>
612                 /// Advances the MySqlDataReader to the next record.
613                 /// </summary>
614                 /// <returns></returns>
615                 public bool Read()
616                 {
617                         if (! isOpen)
618                                 throw new MySqlException("Invalid attempt to Read when reader is closed.");
619
620                         if (! canRead) return false;
621
622                         Driver driver = connection.InternalConnection.Driver;
623                         connection.SetState( ConnectionState.Fetching );
624
625                         try 
626                         {
627                                 Packet rowPacket = driver.ReadPacket();
628                                 if (rowPacket.Type == PacketType.Last) 
629                                 {
630                                         canRead = false;
631                                         return false;
632                                 }
633                                 rowPacket.Position = 0;
634
635                                 for (int col=0; col < _fields.Length; col++)
636                                 {
637                                         int len = (int)rowPacket.ReadLenInteger();
638                                         _fields[col].SetValueData( rowPacket.GetBytes(), (int)rowPacket.Position, len, driver.Encoding );
639                                         rowPacket.Position += len;
640                                 }
641                         }
642                         catch (Exception ex)
643                         {
644                                 System.Diagnostics.Trace.WriteLine("MySql error: " + ex.Message);
645                                 throw ex;
646                         }
647                         finally 
648                         {
649                                 connection.SetState( ConnectionState.Open );
650                         }
651                         return true;
652                 }
653
654
655                 /*
656                 * Implementation specific methods.
657                 */
658                 private int _cultureAwareCompare(string strA, string strB)
659                 {
660         //      return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, strB, CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase);
661                         return 0;
662                 }
663
664                 #region Private Methods
665
666                 private void ClearCurrentResult() 
667                 {
668                         if (! canRead) return;
669
670                         Packet packet = connection.InternalConnection.Driver.ReadPacket();
671                         // clean out any current resultset
672                         while (packet.Type != PacketType.Last)
673                                 packet = connection.InternalConnection.Driver.ReadPacket();
674                 }
675
676                 #endregion
677
678                 #region IEnumerator
679                 public IEnumerator      GetEnumerator()
680                 {
681                         return new System.Data.Common.DbEnumerator(this);
682                 }
683                 #endregion
684   }
685 }