From b6075f1b0b43e618d7e13baead58c0374a1c0fca Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Tue, 20 Oct 2026 08:39:44 -0500 Subject: [PATCH] Make HasRows report whether rows exist --- .../FbDataReaderTests.cs | 33 +++++++++++++++ .../FirebirdClient/FbDataReader.cs | 40 +++++++++++++++++-- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs index c87c804bc..5828dffac 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs @@ -54,6 +54,39 @@ 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(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 ReadClobTest() { diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs index 0cf1d40c4..903fc03c8 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs @@ -51,6 +51,9 @@ public sealed class FbDataReader : DbDataReader private bool _eof; private bool _isClosed; private int _position; + private DbValue[] _peekedRow; + private bool _hasPeekedRow; + private bool _hasRows; private int _recordsAffected; private Dictionary _columnsIndexesOrdinal; private Dictionary _columnsIndexesOrdinalCI; @@ -102,9 +105,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 @@ -166,6 +185,8 @@ public override void Close() _command = null; _connection = null; _row = null; + _peekedRow = null; + _hasPeekedRow = false; _schemaTable = null; _fields = null; } @@ -195,6 +216,8 @@ public override async Task CloseAsync() _command = null; _connection = null; _row = null; + _peekedRow = null; + _hasPeekedRow = false; _schemaTable = null; _fields = null; } @@ -229,10 +252,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 +283,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 +1037,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)