// Copyright (c) 2004-2008 MySQL AB, 2008-2009 Sun Microsystems, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as published by // the Free Software Foundation // // There are special exceptions to the terms and conditions of the GPL // as it is applied to this software. View the full text of the // exception in file EXCEPTIONS in the directory of this software // distribution. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Data; using System.Data.Common; using System.Collections; using MySql.Data.Types; using System.Data.SqlTypes; using System.Collections.Generic; using System.Globalization; using MySql.Data.MySqlClient.Properties; namespace MySql.Data.MySqlClient { /// public sealed class MySqlDataReader : DbDataReader, IDataReader, IDataRecord { // The DataReader should always be open when returned to the user. private bool isOpen = true; private CommandBehavior commandBehavior; private MySqlCommand command; internal long affectedRows; internal Driver driver; private PreparableStatement statement; private ResultSet resultSet; /* * Keep track of the connection in order to implement the * CommandBehavior.CloseConnection flag. A null reference means * normal behavior (do not automatically close). */ private MySqlConnection connection; /* * Because the user should not be able to directly create a * DataReader object, the constructors are * marked as internal. */ internal MySqlDataReader(MySqlCommand cmd, PreparableStatement statement, CommandBehavior behavior) { this.command = cmd; connection = (MySqlConnection)command.Connection; commandBehavior = behavior; driver = connection.driver; affectedRows = -1; this.statement = statement; } #region Properties internal PreparableStatement Statement { get { return statement; } } internal MySqlCommand Command { get { return command; } } internal ResultSet ResultSet { get { return resultSet; } } /// /// Gets a value indicating the depth of nesting for the current row. This method is not /// supported currently and always returns 0. /// public override int Depth { get { return 0; } } /// /// Gets the number of columns in the current row. /// public override int FieldCount { get { return resultSet == null ? 0 : resultSet.Size; } } /// /// Gets a value indicating whether the MySqlDataReader contains one or more rows. /// public override bool HasRows { get { return resultSet == null ? false : resultSet.HasRows; } } /// /// Gets a value indicating whether the data reader is closed. /// public override bool IsClosed { get { return !isOpen; } } /// /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement. /// public override int RecordsAffected { // RecordsAffected returns the number of rows affected in batch // statments from insert/delete/update statments. This property // is not completely accurate until .Close() has been called. get { return (int)affectedRows; } } /// /// Overloaded. Gets the value of a column in its native format. /// In C#, this property is the indexer for the MySqlDataReader class. /// public override object this[int i] { get { return GetValue(i); } } /// /// Gets the value of a column in its native format. /// [C#] In C#, this property is the indexer for the MySqlDataReader class. /// public override object this[String name] { // Look up the ordinal and return // the value at that position. get { return this[GetOrdinal(name)]; } } #endregion /// /// Closes the MySqlDataReader object. /// public override void Close() { if (!isOpen) return; bool shouldCloseConnection = (commandBehavior & CommandBehavior.CloseConnection) != 0; commandBehavior = CommandBehavior.Default; // clear all remaining resultsets try { while (NextResult()) { } } catch (MySqlException ex) { if (ex.Number != 1317) throw; } connection.Reader = null; // we now give the command a chance to terminate. In the case of // stored procedures it needs to update out and inout parameters command.Close(this); if (shouldCloseConnection) connection.Close(); command = null; connection = null; isOpen = false; } #region TypeSafe Accessors /// /// Gets the value of the specified column as a Boolean. /// /// /// public bool GetBoolean(string name) { return GetBoolean(GetOrdinal(name)); } /// /// Gets the value of the specified column as a Boolean. /// /// /// public override bool GetBoolean(int i) { return Convert.ToBoolean(GetValue(i)); } /// /// Gets the value of the specified column as a byte. /// /// /// public byte GetByte(string name) { return GetByte(GetOrdinal(name)); } /// /// Gets the value of the specified column as a byte. /// /// /// public override byte GetByte(int i) { IMySqlValue v = GetFieldValue(i, false); if (v is MySqlUByte) return ((MySqlUByte)v).Value; else return (byte)((MySqlByte)v).Value; } /// /// Gets the value of the specified column as a sbyte. /// /// /// public sbyte GetSByte(string name) { return GetSByte(GetOrdinal(name)); } /// /// Gets the value of the specified column as a sbyte. /// /// /// public sbyte GetSByte(int i) { IMySqlValue v = GetFieldValue(i, false); if (v is MySqlByte) return ((MySqlByte)v).Value; else return (sbyte)((MySqlByte)v).Value; } /// /// Reads a stream of bytes from the specified column offset into the buffer an array starting at the given buffer offset. /// /// The zero-based column ordinal. /// The index within the field from which to begin the read operation. /// The buffer into which to read the stream of bytes. /// The index for buffer to begin the read operation. /// The maximum length to copy into the buffer. /// The actual number of bytes read. /// public override long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) { if (i >= FieldCount) throw new IndexOutOfRangeException(); IMySqlValue val = GetFieldValue(i, false); if (!(val is MySqlBinary) && !(val is MySqlGuid)) throw new MySqlException("GetBytes can only be called on binary or guid columns"); byte[] bytes = null; if (val is MySqlBinary) bytes = ((MySqlBinary)val).Value; else bytes = ((MySqlGuid)val).Bytes; if (buffer == null) return bytes.Length; if (bufferoffset >= buffer.Length || bufferoffset < 0) throw new IndexOutOfRangeException("Buffer index must be a valid index in buffer"); if (buffer.Length < (bufferoffset + length)) throw new ArgumentException("Buffer is not large enough to hold the requested data"); if (fieldOffset < 0 || ((ulong)fieldOffset >= (ulong)bytes.Length && (ulong)bytes.Length > 0)) throw new IndexOutOfRangeException("Data index must be a valid index in the field"); // adjust the length so we don't run off the end if ((ulong)bytes.Length < (ulong)(fieldOffset + length)) { length = (int)((ulong)bytes.Length - (ulong)fieldOffset); } Buffer.BlockCopy(bytes, (int)fieldOffset, buffer, (int)bufferoffset, (int)length); return length; } private object ChangeType(IMySqlValue value, int fieldIndex, Type newType) { #if !CF resultSet.Fields[fieldIndex].AddTypeConversion(newType); #endif return Convert.ChangeType(value.Value, newType, CultureInfo.InvariantCulture); } /// /// Gets the value of the specified column as a single character. /// /// /// public char GetChar(string name) { return GetChar(GetOrdinal(name)); } /// /// Gets the value of the specified column as a single character. /// /// /// public override char GetChar(int i) { string s = GetString(i); return s[0]; } /// /// Reads a stream of characters from the specified column offset into the buffer as an array starting at the given buffer offset. /// /// /// /// /// /// /// public override long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) { if (i >= FieldCount) throw new IndexOutOfRangeException(); string valAsString = GetString(i); if (buffer == null) return valAsString.Length; if (bufferoffset >= buffer.Length || bufferoffset < 0) throw new IndexOutOfRangeException("Buffer index must be a valid index in buffer"); if (buffer.Length < (bufferoffset + length)) throw new ArgumentException("Buffer is not large enough to hold the requested data"); if (fieldoffset < 0 || fieldoffset >= valAsString.Length) throw new IndexOutOfRangeException("Field offset must be a valid index in the field"); if (valAsString.Length < length) length = valAsString.Length; valAsString.CopyTo((int)fieldoffset, buffer, bufferoffset, length); return length; } /// /// Gets the name of the source data type. /// /// /// public override String GetDataTypeName(int i) { if (!isOpen) throw new Exception("No current query in data reader"); if (i >= FieldCount) throw new IndexOutOfRangeException(); // return the name of the type used on the backend IMySqlValue v = resultSet.Values[i]; return v.MySqlTypeName; } /// public MySqlDateTime GetMySqlDateTime(string column) { return GetMySqlDateTime(GetOrdinal(column)); } /// public MySqlDateTime GetMySqlDateTime(int column) { return (MySqlDateTime)GetFieldValue(column, true); } /// public DateTime GetDateTime(string column) { return GetDateTime(GetOrdinal(column)); } /// public override DateTime GetDateTime(int i) { IMySqlValue val = GetFieldValue(i, true); MySqlDateTime dt; if (val is MySqlDateTime) dt = (MySqlDateTime)val; else { // we need to do this because functions like date_add return string string s = GetString(i); dt = MySqlDateTime.Parse(s); } if (connection.Settings.ConvertZeroDateTime && !dt.IsValidDateTime) return DateTime.MinValue; else return dt.GetDateTime(); } public MySqlDecimal GetMySqlDecimal(string column) { return GetMySqlDecimal(GetOrdinal(column)); } public MySqlDecimal GetMySqlDecimal(int i) { return (MySqlDecimal)GetFieldValue(i, false); } /// public Decimal GetDecimal(string column) { return GetDecimal(GetOrdinal(column)); } /// public override Decimal GetDecimal(int i) { IMySqlValue v = GetFieldValue(i, true); if (v is MySqlDecimal) return ((MySqlDecimal)v).Value; return Convert.ToDecimal(v.Value); } /// public double GetDouble(string column) { return GetDouble(GetOrdinal(column)); } /// public override double GetDouble(int i) { IMySqlValue v = GetFieldValue(i, true); if (v is MySqlDouble) return ((MySqlDouble)v).Value; return Convert.ToDouble(v.Value); } public Type GetFieldType(string column) { return GetFieldType(GetOrdinal(column)); } /// /// Gets the Type that is the data type of the object. /// /// /// public override Type GetFieldType(int i) { if (!isOpen) throw new Exception("No current query in data reader"); if (i >= FieldCount) throw new IndexOutOfRangeException(); // we have to use the values array directly because we can't go through // GetValue IMySqlValue v = resultSet.Values[i]; if (v is MySqlDateTime) { if (!connection.Settings.AllowZeroDateTime) return typeof(DateTime); return typeof(MySqlDateTime); } return v.SystemType; } /// public float GetFloat(string column) { return GetFloat(GetOrdinal(column)); } /// public override float GetFloat(int i) { IMySqlValue v = GetFieldValue(i, true); if (v is MySqlSingle) return ((MySqlSingle)v).Value; return Convert.ToSingle(v.Value); } /// public Guid GetGuid(string column) { return GetGuid(GetOrdinal(column)); } /// public override Guid GetGuid(int i) { object v = GetValue(i); if (v is Guid) return (Guid)v; if (v is string) return new Guid(v as string); if (v is byte[]) { byte[] bytes = (byte[])v; if (bytes.Length == 16) return new Guid(bytes); } throw new MySqlException(Resources.ValueNotSupportedForGuid); } /// public Int16 GetInt16(string column) { return GetInt16(GetOrdinal(column)); } /// public override Int16 GetInt16(int i) { IMySqlValue v = GetFieldValue(i, true); if (v is MySqlInt16) return ((MySqlInt16)v).Value; return (short)ChangeType(v, i, typeof(short)); } /// public Int32 GetInt32(string column) { return GetInt32(GetOrdinal(column)); } /// public override Int32 GetInt32(int i) { IMySqlValue v = GetFieldValue(i, true); if (v is MySqlInt32) return ((MySqlInt32)v).Value; return (Int32)ChangeType(v, i, typeof(Int32)); } /// public Int64 GetInt64(string column) { return GetInt64(GetOrdinal(column)); } /// public override Int64 GetInt64(int i) { IMySqlValue v = GetFieldValue(i, true); if (v is MySqlInt64) return ((MySqlInt64)v).Value; return (Int64)ChangeType(v, i, typeof(Int64)); } /// /// Gets the name of the specified column. /// /// /// public override String GetName(int i) { if (!isOpen) throw new Exception("No current query in data reader"); if (i >= FieldCount) throw new IndexOutOfRangeException(); return resultSet.Fields[i].ColumnName; } /// /// Gets the column ordinal, given the name of the column. /// /// /// public override int GetOrdinal(string name) { if (!isOpen || resultSet == null) throw new Exception("No current query in data reader"); return resultSet.GetOrdinal(name); } /// /// Returns a DataTable that describes the column metadata of the MySqlDataReader. /// /// public override DataTable GetSchemaTable() { // Only Results from SQL SELECT Queries // get a DataTable for schema of the result // otherwise, DataTable is null reference if (FieldCount == 0) return null; DataTable dataTableSchema = new DataTable("SchemaTable"); dataTableSchema.Columns.Add("ColumnName", typeof(string)); dataTableSchema.Columns.Add("ColumnOrdinal", typeof(int)); dataTableSchema.Columns.Add("ColumnSize", typeof(int)); dataTableSchema.Columns.Add("NumericPrecision", typeof(int)); dataTableSchema.Columns.Add("NumericScale", typeof(int)); dataTableSchema.Columns.Add("IsUnique", typeof(bool)); dataTableSchema.Columns.Add("IsKey", typeof(bool)); DataColumn dc = dataTableSchema.Columns["IsKey"]; dc.AllowDBNull = true; // IsKey can have a DBNull dataTableSchema.Columns.Add("BaseCatalogName", typeof(string)); dataTableSchema.Columns.Add("BaseColumnName", typeof(string)); dataTableSchema.Columns.Add("BaseSchemaName", typeof(string)); dataTableSchema.Columns.Add("BaseTableName", typeof(string)); dataTableSchema.Columns.Add("DataType", typeof(Type)); dataTableSchema.Columns.Add("AllowDBNull", typeof(bool)); dataTableSchema.Columns.Add("ProviderType", typeof(int)); dataTableSchema.Columns.Add("IsAliased", typeof(bool)); dataTableSchema.Columns.Add("IsExpression", typeof(bool)); dataTableSchema.Columns.Add("IsIdentity", typeof(bool)); dataTableSchema.Columns.Add("IsAutoIncrement", typeof(bool)); dataTableSchema.Columns.Add("IsRowVersion", typeof(bool)); dataTableSchema.Columns.Add("IsHidden", typeof(bool)); dataTableSchema.Columns.Add("IsLong", typeof(bool)); dataTableSchema.Columns.Add("IsReadOnly", typeof(bool)); int ord = 1; for (int i = 0; i < FieldCount; i++) { MySqlField f = resultSet.Fields[i]; DataRow r = dataTableSchema.NewRow(); r["ColumnName"] = f.ColumnName; r["ColumnOrdinal"] = ord++; r["ColumnSize"] = f.IsTextField ? f.ColumnLength / f.MaxLength : f.ColumnLength; int prec = f.Precision; int pscale = f.Scale; if (prec != -1) r["NumericPrecision"] = (short)prec; if (pscale != -1) r["NumericScale"] = (short)pscale; r["DataType"] = GetFieldType(i); r["ProviderType"] = (int)f.Type; r["IsLong"] = f.IsBlob && f.ColumnLength > 255; r["AllowDBNull"] = f.AllowsNull; r["IsReadOnly"] = false; r["IsRowVersion"] = false; r["IsUnique"] = f.IsUnique; r["IsKey"] = f.IsPrimaryKey; r["IsAutoIncrement"] = f.IsAutoIncrement; r["BaseSchemaName"] = f.DatabaseName; r["BaseCatalogName"] = null; r["BaseTableName"] = f.RealTableName; r["BaseColumnName"] = f.OriginalColumnName; dataTableSchema.Rows.Add(r); } return dataTableSchema; } /// public string GetString(string column) { return GetString(GetOrdinal(column)); } /// public override String GetString(int i) { IMySqlValue val = GetFieldValue(i, true); if (val is MySqlBinary) { byte[] v = ((MySqlBinary)val).Value; return resultSet.Fields[i].Encoding.GetString(v, 0, v.Length); } return val.Value.ToString(); } /// public TimeSpan GetTimeSpan(string column) { return GetTimeSpan(GetOrdinal(column)); } /// public TimeSpan GetTimeSpan(int column) { IMySqlValue val = GetFieldValue(column, true); MySqlTimeSpan ts = (MySqlTimeSpan)val; return ts.Value; } /// /// Gets the value of the specified column in its native format. /// /// /// public override object GetValue(int i) { if (!isOpen) throw new Exception("No current query in data reader"); if (i >= FieldCount) throw new IndexOutOfRangeException(); IMySqlValue val = GetFieldValue(i, false); if (val.IsNull) return DBNull.Value; // if the column is a date/time, then we return a MySqlDateTime // so .ToString() will print '0000-00-00' correctly if (val is MySqlDateTime) { MySqlDateTime dt = (MySqlDateTime)val; if (!dt.IsValidDateTime && connection.Settings.ConvertZeroDateTime) return DateTime.MinValue; else if (connection.Settings.AllowZeroDateTime) return val; else return dt.GetDateTime(); } return val.Value; } /// /// Gets all attribute columns in the collection for the current row. /// /// /// public override int GetValues(object[] values) { int numCols = Math.Min(values.Length, FieldCount); for (int i = 0; i < numCols; i++) values[i] = GetValue(i); return numCols; } /// public UInt16 GetUInt16(string column) { return GetUInt16(GetOrdinal(column)); } /// public UInt16 GetUInt16(int column) { IMySqlValue v = GetFieldValue(column, true); if (v is MySqlUInt16) return ((MySqlUInt16)v).Value; return (UInt16)ChangeType(v, column, typeof(UInt16)); } /// public UInt32 GetUInt32(string column) { return GetUInt32(GetOrdinal(column)); } /// public UInt32 GetUInt32(int column) { IMySqlValue v = GetFieldValue(column, true); if (v is MySqlUInt32) return ((MySqlUInt32)v).Value; return (uint)ChangeType(v, column, typeof(UInt32)); } /// public UInt64 GetUInt64(string column) { return GetUInt64(GetOrdinal(column)); } /// public UInt64 GetUInt64(int column) { IMySqlValue v = GetFieldValue(column, true); if (v is MySqlUInt64) return ((MySqlUInt64)v).Value; return (UInt64)ChangeType(v, column, typeof(UInt64)); } #endregion IDataReader IDataRecord.GetData(int i) { return base.GetData(i); } /// /// Gets a value indicating whether the column contains non-existent or missing values. /// /// /// public override bool IsDBNull(int i) { return DBNull.Value == GetValue(i); } /// /// Advances the data reader to the next result, when reading the results of batch SQL statements. /// /// public override bool NextResult() { if (!isOpen) throw new MySqlException(Resources.NextResultIsClosed); // this will clear out any unread data if (resultSet != null) resultSet.Close(); // single result means we only return a single resultset. If we have already // returned one, then we return false; if (resultSet != null && (commandBehavior & CommandBehavior.SingleResult) != 0) return false; // next load up the next resultset if any try { do { resultSet = null; resultSet = driver.NextResult(Statement.StatementId); if (resultSet == null) return false; if (resultSet.IsOutputParameters) return false; if (resultSet.Size == 0) { Command.lastInsertedId = resultSet.InsertedId; if (affectedRows == -1) affectedRows = resultSet.AffectedRows; else affectedRows += resultSet.AffectedRows; } } while (resultSet.Size == 0); return true; } catch (MySqlException ex) { if (ex.IsFatal) connection.Abort(); if (ex.Number == 0) throw new MySqlException(Resources.FatalErrorReadingResult, ex); throw; } } /// /// Advances the MySqlDataReader to the next record. /// /// public override bool Read() { if (!isOpen) throw new MySqlException("Invalid attempt to Read when reader is closed."); if (resultSet == null) return false; try { return resultSet.NextRow(commandBehavior); } catch (TimeoutException tex) { connection.HandleTimeout(tex); return false; // unreached } catch (MySqlException ex) { if (ex.IsFatal) connection.Abort(); // if we get a query interrupted then our resultset is done if (ex.Number == 1317) { return false; } throw new MySqlException(Resources.FatalErrorDuringRead, ex); } } private IMySqlValue GetFieldValue(int index, bool checkNull) { if (index < 0 || index >= FieldCount) throw new ArgumentException(Resources.InvalidColumnOrdinal); IMySqlValue v = resultSet[index]; if (checkNull && v.IsNull) throw new SqlNullValueException(); return v; } #region IEnumerator /// /// Returns an that iterates through the . /// /// public override IEnumerator GetEnumerator() { return new DbEnumerator(this, (commandBehavior & CommandBehavior.CloseConnection) != 0); } #endregion } }