Skip to content

Follow the ADO.NET contract for HasRows, RecordsAffected, OCTETS GetFieldType and RemoveAt - #1283

Closed
alaricd wants to merge 1 commit into
FirebirdSQL:masterfrom
pengdows:fix/adonet-contract
Closed

alaricd wants to merge 1 commit into
FirebirdSQL:masterfrom
pengdows:fix/adonet-contract

Conversation

@alaricd

@alaricd alaricd commented Oct 20, 2026

Copy link
Copy Markdown

Fixes #1282.

Four places where FirebirdClient departs from the documented ADO.NET contract. Each change quotes the sentence it follows:

  • HasRows (DbDataReader.HasRows: "true if the DbDataReader contains one or more rows; otherwise false.")
    It returned _command.HasFields. Now, before the first Read, it fetches one row and keeps it, and the next Read/ReadAsync returns that row instead of fetching. After a row has been read, HasRows stays true. SchemaOnly, a closed reader, and statements without fields still return false without fetching. Closing the reader discards the buffered row.
  • RecordsAffected (DbDataReader.RecordsAffected: "-1 for SELECT statements; 0 if no rows were affected or the statement failed.")
    _recordsAffected now starts at -1, the sentinel UpdateRecordsAffected already checks. The reader takes its count from a new FbCommand.StatementRecordsAffected, the INSERT/UPDATE/DELETE switch that ExecuteNonQuery and ExecuteNonQueryAsync already used, moved into one place.
  • GetFieldType for OCTETS (IDataRecord.GetFieldType: "the type of Object that would be returned from GetValue.")
    DbField.GetSystemType returns byte[] for CHAR/VARCHAR in the OCTETS character set, matching what DbField.SetValue stores. CHAR(16) OCTETS keeps mapping to Guid. DbDataType is unchanged, so parameter handling isn't affected.
  • RemoveAt(int) (implements IList.RemoveAt: ArgumentOutOfRangeException when "index is not a valid index in the IList.")
    Now throws ArgumentOutOfRangeException (ParamName index), and also for index == Count, which the old index > Count check let through to the indexer. RemoveAt(string) keeps throwing IndexOutOfRangeException for an unknown name.

Backward compatibility

  • Code that relied on HasRows being true for an empty result, or on RecordsAffected being 0 for a SELECT, sees the documented values. A text-mode EXECUTE PROCEDURE run through a reader now reports -1, as ExecuteNonQuery already did.
  • catch (IndexOutOfRangeException) around Parameters.RemoveAt(int) needs ArgumentOutOfRangeException.
  • The first HasRows call fetches a row. That's one fetch the following Read would have done anyway.

Tests

New tests:

  • FbDataReaderTests: HasRowsIsFalseForAnEmptyResultTest, HasRowsDoesNotSkipTheFirstRowTest, RecordsAffectedIsMinusOneForSelectTest, GetFieldTypeMatchesGetValueForOctetsTest.
  • FbParameterCollectionTests: RemoveAtInvalidIndexThrowsArgumentOutOfRangeTest (-1, 0 on an empty collection, Count) and RemoveAtUnknownNameThrowsIndexOutOfRangeTest.

I ran the whole FirebirdSql.Data.FirebirdClient.Tests suite (the non-embedded fixtures) against Firebird 5.0.2 in Docker (WireCrypt = Enabled), before and after the change:

  • Before: 1848 passed, 72 failed.
  • After: 1868 passed, 72 failed. The 20 extra passes are the new tests across the four server fixtures.

The 72 failures are the same tests both times. They're environmental: backup/restore streaming, shutdown and services tests that need server-side file access this container doesn't give. I couldn't run the embedded fixtures here.

Found by the pengdows ADO.NET conformance suite, which runs the documented System.Data.Common contract against every provider.

AI disclosure: the analysis, the change, the tests and this description were produced with Claude Code (an AI assistant).

…ieldType and RemoveAt (FirebirdSQL#1282)

- HasRows reports whether the result has rows: before the first Read it
  fetches one row ahead, which Read then returns
- RecordsAffected starts at -1 and counts only INSERT, UPDATE and DELETE,
  as ExecuteNonQuery already does
- GetFieldType is byte[] for CHAR and VARCHAR in the OCTETS character set,
  the type GetValue returns
- RemoveAt(int) throws ArgumentOutOfRangeException for an index outside
  the collection, including Count; RemoveAt(string) keeps
  IndexOutOfRangeException for an unknown name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cincuranet

Copy link
Copy Markdown
Member

Please create separate PRs for each issue (and separate issues to link to).

@cincuranet cincuranet closed this Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HasRows, RecordsAffected, GetFieldType for OCTETS and RemoveAt(int) depart from the ADO.NET contract

2 participants