You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

321 lines
10 KiB

#if MYSQL_6_10
// Copyright © 2009, 2018, Oracle and/or its affiliates. All rights reserved.
//
// MySQL Connector/NET is licensed under the terms of the GPLv2
// <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most
// MySQL Connectors. There are special exceptions to the terms and
// conditions of the GPLv2 as it is applied to this software, see the
// FLOSS License Exception
// <http://www.mysql.com/about/legal/licensing/foss-exception.html>.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 2 of the License.
//
// 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.,
// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using Externals.MySql.Data.Types;
namespace Externals.MySql.Data.MySqlClient
{
internal class ResultSet
{
private Driver _driver;
private bool[] _uaFieldsUsed;
private Dictionary<string, int> _fieldHashCi;
private int _rowIndex;
private bool _readDone;
private bool _isSequential;
private int _seqIndex;
private readonly int _statementId;
private bool _cached;
private List<IMySqlValue[]> _cachedValues;
public ResultSet(int affectedRows, long insertedId)
{
AffectedRows = affectedRows;
InsertedId = insertedId;
_readDone = true;
}
public ResultSet(Driver d, int statementId, int numCols)
{
AffectedRows = -1;
InsertedId = -1;
_driver = d;
_statementId = statementId;
_rowIndex = -1;
LoadColumns(numCols);
IsOutputParameters = IsOutputParameterResultSet();
HasRows = GetNextRow();
_readDone = !HasRows;
}
#region Properties
public bool HasRows { get; }
public int Size => Fields?.Length ?? 0;
public MySqlField[] Fields { get; private set; }
public IMySqlValue[] Values { get; private set; }
public bool IsOutputParameters { get; set; }
public int AffectedRows { get; private set; }
public long InsertedId { get; private set; }
public int TotalRows { get; private set; }
public int SkippedRows { get; private set; }
public bool Cached
{
get { return _cached; }
set
{
_cached = value;
if (_cached && _cachedValues == null)
_cachedValues = new List<IMySqlValue[]>();
}
}
#endregion
/// <summary>
/// return the ordinal for the given column name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public int GetOrdinal(string name)
{
int ordinal;
// quick hash lookup using CI hash
if (_fieldHashCi.TryGetValue(name, out ordinal))
return ordinal;
// Throw an exception if the ordinal cannot be found.
throw new IndexOutOfRangeException(
String.Format(Resources.CouldNotFindColumnName, name));
}
/// <summary>
/// Retrieve the value as the given column index
/// </summary>
/// <param name="index">The column value to retrieve</param>
/// <returns>The value as the given column</returns>
public IMySqlValue this[int index]
{
get
{
if (_rowIndex < 0)
throw new MySqlException(Resources.AttemptToAccessBeforeRead);
// keep count of how many columns we have left to access
_uaFieldsUsed[index] = true;
if (_isSequential && index != _seqIndex)
{
if (index < _seqIndex)
throw new MySqlException(Resources.ReadingPriorColumnUsingSeqAccess);
while (_seqIndex < (index - 1))
_driver.SkipColumnValue(Values[++_seqIndex]);
Values[index] = _driver.ReadColumnValue(index, Fields[index], Values[index]);
_seqIndex = index;
}
return Values[index];
}
}
private bool GetNextRow()
{
bool fetched = _driver.FetchDataRow(_statementId, Size);
if (fetched)
TotalRows++;
return fetched;
}
public bool NextRow(CommandBehavior behavior)
{
if (_readDone)
{
if (Cached) return CachedNextRow(behavior);
return false;
}
if ((behavior & CommandBehavior.SingleRow) != 0 && _rowIndex == 0)
return false;
_isSequential = (behavior & CommandBehavior.SequentialAccess) != 0;
_seqIndex = -1;
// if we are at row index >= 0 then we need to fetch the data row and load it
if (_rowIndex >= 0)
{
bool fetched = false;
try
{
fetched = GetNextRow();
}
catch (MySqlException ex)
{
if (ex.IsQueryAborted)
{
// avoid hanging on Close()
_readDone = true;
}
throw;
}
if (!fetched)
{
_readDone = true;
return false;
}
}
if (!_isSequential) ReadColumnData(false);
_rowIndex++;
return true;
}
private bool CachedNextRow(CommandBehavior behavior)
{
if ((behavior & CommandBehavior.SingleRow) != 0 && _rowIndex == 0)
return false;
if (_rowIndex == (TotalRows - 1)) return false;
_rowIndex++;
Values = _cachedValues[_rowIndex];
return true;
}
/// <summary>
/// Closes the current resultset, dumping any data still on the wire
/// </summary>
public void Close()
{
if (!_readDone)
{
// if we have rows but the user didn't read the first one then mark it as skipped
if (HasRows && _rowIndex == -1)
SkippedRows++;
try
{
while (_driver.IsOpen && _driver.SkipDataRow())
{
TotalRows++;
SkippedRows++;
}
}
catch (System.IO.IOException)
{
// it is ok to eat IO exceptions here, we just want to
// close the result set
}
_readDone = true;
}
else if (_driver == null)
CacheClose();
_driver = null;
if (Cached) CacheReset();
}
private void CacheClose()
{
SkippedRows = TotalRows - _rowIndex - 1;
}
private void CacheReset()
{
if (!Cached) return;
_rowIndex = -1;
AffectedRows = -1;
InsertedId = -1;
SkippedRows = 0;
}
public bool FieldRead(int index)
{
Debug.Assert(Size > index);
return _uaFieldsUsed[index];
}
public void SetValueObject(int i, IMySqlValue valueObject)
{
Debug.Assert(Values != null);
Debug.Assert(i < Values.Length);
Values[i] = valueObject;
}
private bool IsOutputParameterResultSet()
{
if (_driver.HasStatus(ServerStatusFlags.OutputParameters)) return true;
if (Fields.Length == 0) return false;
for (int x = 0; x < Fields.Length; x++)
if (!Fields[x].ColumnName.StartsWith("@" + StoredProcedure.ParameterPrefix, StringComparison.OrdinalIgnoreCase)) return false;
return true;
}
/// <summary>
/// Loads the column metadata for the current resultset
/// </summary>
private void LoadColumns(int numCols)
{
Fields = _driver.GetColumns(numCols);
Values = new IMySqlValue[numCols];
_uaFieldsUsed = new bool[numCols];
_fieldHashCi = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < Fields.Length; i++)
{
string columnName = Fields[i].ColumnName;
if (!_fieldHashCi.ContainsKey(columnName))
_fieldHashCi.Add(columnName, i);
Values[i] = Fields[i].GetValueObject();
}
}
private void ReadColumnData(bool outputParms)
{
for (int i = 0; i < Size; i++)
Values[i] = _driver.ReadColumnValue(i, Fields[i], Values[i]);
// if we are caching then we need to save a copy of this row of data values
if (Cached)
_cachedValues.Add((IMySqlValue[])Values.Clone());
// we don't need to worry about caching the following since you won't have output
// params with TableDirect commands
if (!outputParms) return;
bool rowExists = _driver.FetchDataRow(_statementId, Fields.Length);
_rowIndex = 0;
if (rowExists)
throw new MySqlException(Resources.MoreThanOneOPRow);
}
}
}
#endif