diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs index c87c804b..05b7773a 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs @@ -54,6 +54,90 @@ public async Task ReadTest() } } + [Test] + public async Task HasRowsIsFalseForAnEmptyResultTest() + { + // DbDataReader.HasRows: "true if the DbDataReader contains one or more rows; otherwise false." + await using (var command = new FbCommand("select INT_FIELD from TEST where 1 = 0", Connection)) + { + await using (var reader = await command.ExecuteReaderAsync()) + { + Assert.IsFalse(reader.HasRows); + Assert.IsFalse(await reader.ReadAsync()); + } + } + } + + [Test] + public async Task HasRowsDoesNotSkipTheFirstRowTest() + { + await using (var command = new FbCommand("select INT_FIELD from TEST where INT_FIELD in (1, 2) order by INT_FIELD", Connection)) + { + await using (var reader = await command.ExecuteReaderAsync()) + { + Assert.IsTrue(reader.HasRows); + Assert.IsTrue(await reader.ReadAsync()); + Assert.AreEqual(1, reader.GetInt32(0)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual(2, reader.GetInt32(0)); + Assert.IsFalse(reader.Read()); + Assert.IsTrue(reader.HasRows, "HasRows describes the result, not the current position"); + } + } + } + + [Test] + public async Task HasRowsThenCloseKeepsStoredProcedureOutputParametersTest() + { + await using (var create = Connection.CreateCommand()) + { + create.CommandText = "RECREATE PROCEDURE HASROWS_OUTPUT (P1 INTEGER) RETURNS (R1 INTEGER) AS BEGIN R1 = P1 * 2; END"; + await create.ExecuteNonQueryAsync(); + } + + await using (var command = new FbCommand("EXECUTE PROCEDURE HASROWS_OUTPUT(?)", Connection)) + { + command.CommandType = CommandType.StoredProcedure; + command.Parameters.Add("P1", FbDbType.Integer).Value = 21; + var output = command.Parameters.Add("R1", FbDbType.Integer); + output.Direction = ParameterDirection.Output; + + await using (var reader = await command.ExecuteReaderAsync()) + { + Assert.IsTrue(reader.HasRows); + await reader.CloseAsync(); + } + + Assert.AreEqual(42, output.Value, "HasRows must not consume the stored procedure output row"); + } + } + + [Test] + public void HasRowsThenCloseKeepsStoredProcedureOutputParametersSyncTest() + { + using (var create = Connection.CreateCommand()) + { + create.CommandText = "RECREATE PROCEDURE HASROWS_OUTPUT (P1 INTEGER) RETURNS (R1 INTEGER) AS BEGIN R1 = P1 * 2; END"; + create.ExecuteNonQuery(); + } + + using (var command = new FbCommand("EXECUTE PROCEDURE HASROWS_OUTPUT(?)", Connection)) + { + command.CommandType = CommandType.StoredProcedure; + command.Parameters.Add("P1", FbDbType.Integer).Value = 21; + var output = command.Parameters.Add("R1", FbDbType.Integer); + output.Direction = ParameterDirection.Output; + + using (var reader = command.ExecuteReader()) + { + Assert.IsTrue(reader.HasRows); + reader.Close(); + } + + Assert.AreEqual(42, output.Value, "HasRows must not consume the stored procedure output row"); + } + } + [Test] public async Task ReadClobTest() { diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs index 0cf1d40c..c9e53707 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs @@ -51,6 +51,12 @@ public sealed class FbDataReader : DbDataReader private bool _eof; private bool _isClosed; private int _position; + // Row fetched ahead by HasRows. _hasPeekedRow is needed because a peek that found nothing + // leaves _peekedRow null, and that must still count as "already fetched" so the statement is not + // fetched again. _peekedRow is only non-null while _hasPeekedRow is true. + private DbValue[] _peekedRow; + private bool _hasPeekedRow; + private bool _hasRows; private int _recordsAffected; private Dictionary _columnsIndexesOrdinal; private Dictionary _columnsIndexesOrdinalCI; @@ -102,9 +108,25 @@ public override int Depth } } + /// + /// DbDataReader.HasRows: "true if the DbDataReader contains one or more rows; otherwise false." Before the first + /// Read this fetches one row ahead; Read then returns that row. + /// public override bool HasRows { - get { return _command.HasFields; } + get + { + if (!_hasRows && !_isClosed && !_hasPeekedRow && _position == StartPosition && _command != null && _command.HasFields && !IsCommandBehavior(CommandBehavior.SchemaOnly)) + { + using (var explicitCancellation = ExplicitCancellation.Enter(CancellationToken.None, _command.Cancel)) + { + _peekedRow = _command.Fetch(); + } + _hasPeekedRow = true; + _hasRows = _peekedRow != null; + } + return _hasRows; + } } public override bool IsClosed @@ -150,7 +172,8 @@ public override void Close() { if (_command.CommandType == CommandType.StoredProcedure) { - _command.SetOutputParameters(); + // HasRows may already have fetched the output row; hand it over rather than losing it. + _command.SetOutputParameters(_peekedRow); } if (_command.HasImplicitTransaction) { @@ -166,6 +189,8 @@ public override void Close() _command = null; _connection = null; _row = null; + _peekedRow = null; + _hasPeekedRow = false; _schemaTable = null; _fields = null; } @@ -179,7 +204,8 @@ public override async Task CloseAsync() { if (_command.CommandType == CommandType.StoredProcedure) { - await _command.SetOutputParametersAsync(CancellationToken.None).ConfigureAwait(false); + // HasRows may already have fetched the output row; hand it over rather than losing it. + await _command.SetOutputParametersAsync(_peekedRow, CancellationToken.None).ConfigureAwait(false); } if (_command.HasImplicitTransaction) { @@ -195,6 +221,8 @@ public override async Task CloseAsync() _command = null; _connection = null; _row = null; + _peekedRow = null; + _hasPeekedRow = false; _schemaTable = null; _fields = null; } @@ -229,10 +257,11 @@ public override bool Read() { using (var explicitCancellation = ExplicitCancellation.Enter(CancellationToken.None, _command.Cancel)) { - _row = _command.Fetch(); + _row = TakePeekedRow(out var peeked) ? peeked : _command.Fetch(); if (_row != null) { _position++; + _hasRows = true; return true; } else @@ -259,10 +288,11 @@ public override async Task ReadAsync(CancellationToken cancellationToken) { using (var explicitCancellation = ExplicitCancellation.Enter(cancellationToken, _command.Cancel)) { - _row = await _command.FetchAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); + _row = TakePeekedRow(out var peeked) ? peeked : await _command.FetchAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); if (_row != null) { _position++; + _hasRows = true; return true; } else @@ -1012,6 +1042,15 @@ private FbDbType GetProviderType(int i) return (FbDbType)_fields[i].DbDataType; } + private bool TakePeekedRow(out DbValue[] row) + { + row = _peekedRow; + var hadPeekedRow = _hasPeekedRow; + _peekedRow = null; + _hasPeekedRow = false; + return hadPeekedRow; + } + private void UpdateRecordsAffected() { if (_command != null && !_command.IsDisposed)