Skip to content

Fix garbled diagnostic record on failed SQLDriverConnect#298

Merged
irodushka merged 6 commits into
FirebirdSQL:masterfrom
fdcastel:fix/sqldriverconnect-diag-record
May 15, 2026
Merged

Fix garbled diagnostic record on failed SQLDriverConnect#298
irodushka merged 6 commits into
FirebirdSQL:masterfrom
fdcastel:fix/sqldriverconnect-diag-record

Conversation

@fdcastel
Copy link
Copy Markdown
Member

Summary

Every catch block that handled std::exception followed the same broken pattern:

catch ( std::exception &ex ) {
    SQLException &exception = (SQLException&)ex;
    // virtual calls on `exception`...
}

The C-style cast is a reinterpret_cast. When the caught object was not in fact a SQLException (e.g. a std::bad_alloc, a raw FbException that bypassed the IscDbc rewrap, or anything else that propagated out of the IscDbc layer), the subsequent virtual dispatch through exception read through whatever bytes lay where SQLException's vtable pointer would have been. On Linux that produced a diagnostic record with:

  • empty SQLSTATE,
  • a non-deterministic native code (varying every run),
  • a one-byte message ([) whose reported length disagreed with its strlen.

ODBC clients (unixODBC's isql, pyodbc, Tcl's tdbc::odbc, …) cannot recover useful information from such a record, so application-level retry/branch logic on SQLSTATE or native code is impossible.

What this PR does

  • Introduces an ExceptionInfo helper plus a new postError(state, std::exception&) overload in IscDbc/SQLException.h / OdbcObject. Both route through dynamic_cast<SQLException*>, falling back to std::exception::what() when the caught object isn't a SQLException.
  • Converts all 64 occurrences of the broken pattern across OdbcConnection, OdbcStatement, OdbcDesc, OdbcEnv, and the Windows OdbcJdbcSetup tree to the safe path.
  • Adds a NULL guard around connection->close() in OdbcConnection::connect so a throw inside createConnection() (before connection is assigned) no longer dereferences NULL during cleanup.
  • Adds two regression tests in tests/test_connect_options.cpp:
    • BadPasswordReturnsValidDiagRec — verifies the diag record after a wrong-password failure (skips itself in embedded/trusted-auth setups that don't reject the password, including CI).
    • NonexistentDatabaseReturnsValidDiagRec — verifies the diag record after a connect to a nonexistent database path.

Both tests assert that after SQLDriverConnect fails the diag record has a 5-character SQLSTATE, a reported message length matching strlen of the message, and a message longer than a single byte.

Verification

CI matrix (all green on the head of this PR):

  • Windows x64 / x86 (Win32) / ARM64
  • Linux x64 / ARM64
  • Linux x64 with Valgrind

Tested against Firebird 5.0.3 and the Firebird master snapshot.

Test plan

  • All existing tests still pass on the full Windows + Linux matrix
  • New NonexistentDatabaseReturnsValidDiagRec test passes on all platforms
  • New BadPasswordReturnsValidDiagRec test runs (skips gracefully when the server doesn't reject the bad password)
  • grep -rn '(SQLException&)' *.cpp *.h IscDbc/ OdbcJdbcSetup/ returns no remaining unchecked casts

Two regression tests verify that when SQLDriverConnect fails the
returned diagnostic record is well-formed: a 5-character SQLSTATE, a
message length that matches strlen of the message, and a message body
that is more than a single byte.

The wrong-password test skips itself gracefully if the underlying
Firebird server uses embedded or trusted authentication (the
configuration used by CI), because in that mode the bad password is
not rejected and the failure path can't be exercised.
@fdcastel
Copy link
Copy Markdown
Member Author

fdcastel commented May 11, 2026

@irodushka back from vacation? 😉

Another one similar to #296. As with that one, it would be great if this could make it into -rc2 as well.

This would allow us to run the HammerDB benchmark on Linux.

P.S. I’ve published a new unofficial build based on the latest master plus the 6 currently open PRs at https://github.com/fdcastel/firebird-odbc-driver/releases/tag/v3.5.1-rc1

@irodushka
Copy link
Copy Markdown
Contributor

Hi @fdcastel

Yeah I'm online!
Trying to get back to the stream) Today is te very first day after the vacation, so don't expect too much))

What about this. AI - am I right about that?.. - is trying to solve a trivial bug (that's really very bad, inaccurate, ugly, and the dev hands should be teared off!) with so comlpex & muddy means. A helper foo, and hell, dynamic_cast...

My Geniune Intelligence (I will abbreviate it to GI) tells me, that the much better approach is to separate exception handlers, in a standard way, like this:

try { ... }
catch (const SQLException& ex) {
    ...
}
catch (const std::exception& ex) {
    ...
}

If you're anxious about copypasting - you can use lambda or macrodef.

What about postError() - 1) I cannot understand what's the need for calling the extractExceptionInfo() helper in the overloaded version if you definitely know the exception type here 2) IMHO It's better to make a templated foo, and gate the logic with the if constexpr (std::is_same_v<EX, std::exception>) and so on

@fdcastel
Copy link
Copy Markdown
Member Author

Welcome back, man!

Sorry about this one. It was a quick fix I put together in a rush while trying to solve an issue on Linux. But ultimately I had to drop it (Unicode on Linux strikes again).

I’ll submit an update with your suggestions, and you can tell me what you think. But in the end, we can discard this one if you still don't like it (since I ended up not using it, after all).

@fdcastel fdcastel force-pushed the fix/sqldriverconnect-diag-record branch from c7922bb to ec2e608 Compare May 13, 2026 09:30
Every catch block that handled std::exception followed the same broken
pattern:

    catch ( std::exception &ex ) {
        SQLException &exception = (SQLException&)ex;
        // virtual calls on `exception`...
    }

The C-style cast is a reinterpret_cast. When the caught object was not
in fact a SQLException -- e.g. a std::bad_alloc, a raw FbException
that bypassed the IscDbc rewrap, or anything else that propagated out
of the IscDbc layer -- the subsequent virtual dispatch through
`exception` read through whatever bytes lay where SQLException's
vtable pointer would have been. On Linux that produced a diagnostic
record with an empty SQLSTATE, a non-deterministic native code, and a
one-byte message ('[') whose reported length disagreed with its
strlen.

This commit replaces each single `catch (std::exception &ex)` with the
idiomatic two-handler form:

    catch (SQLException &ex) { ... }
    catch (std::exception &ex) { ... }

C++ exception-matching picks the most-derived handler at runtime, so
the SQLException catch sees only true SQLException objects and the
std::exception catch sees only non-SQLException objects -- no
dynamic_cast or helper required.

A companion postError(state, std::exception&) overload pairs with the
existing postError(state, SQLException&); each catch dispatches the
right one statically based on the type of `ex`.

All 64 occurrences of the broken pattern across OdbcConnection /
OdbcStatement / OdbcDesc / OdbcEnv and the Windows OdbcJdbcSetup tree
are converted to this form.

OdbcConnection::connect gets a small rollbackPartialConnect lambda so
its two catch bodies stay short, plus a NULL guard around
connection->close() -- if createConnection() throws before assignment,
the previous code dereferenced a NULL pointer during cleanup.
@fdcastel
Copy link
Copy Markdown
Member Author

Thanks for the review @irodushka — fully agreed. Pushed ec2e608 which scraps the ExceptionInfo helper / dynamic_cast and rewrites every catch as the standard two-handler form:

catch (SQLException &ex)   { ... ex.getText() / ex.getSqlcode() ... }
catch (std::exception &ex) { ... ex.what() ... }

C++ catch matching picks the right handler statically; no runtime type check.

For postError I kept two simple, non-templated overloads — the existing postError(state, SQLException&) and a one-line postError(state, std::exception&) that just uses ex.what(). Each catch site dispatches to the right one from the static type of ex, so the new overload no longer does any cast.

OdbcConnection::connect keeps a small rollbackPartialConnect lambda to share the cleanup between its two catches (plus the NULL guard for connection->close() from the original PR).

Note on const: stayed with catch (SQLException &ex) (non-const) because getText() / getSqlcode() / getFbcode() are declared non-const virtuals in IscDbc/SQLException.h. Making them const would touch every IscDbc implementation — happy to do that in a follow-up if you'd prefer.

Full matrix is green on the new HEAD: Windows x64 / x86 / ARM64, Linux x64 / ARM64, Linux + Valgrind, against Firebird 5.0.3 and master.

@irodushka
Copy link
Copy Markdown
Contributor

@fdcastel

Look at commit df437ea (yes, I've just pushed a commit to your branch!) It's a simple way to make a const ref to our exceptions. I changed ref to const in the one place only (DsnDialog.cpp). If it works for you - please propagate it.

@irodushka
Copy link
Copy Markdown
Contributor

+++ 86c2d02

Follow-up to df437ea / 86c2d02, which made SQLException's getters const
and updated the postError overloads + DsnDialog.cpp catch site. This
commit applies the same `const` to the remaining 63 catch pairs across
OdbcConnection, OdbcStatement, OdbcDesc, OdbcEnv, and the rest of the
OdbcJdbcSetup tree, so the codebase is uniformly:

    catch (const SQLException &ex)   { ... }
    catch (const std::exception &ex) { ... }

Pure mechanical change; no behavioral effect.
@fdcastel
Copy link
Copy Markdown
Member Author

Done! Pulled both commits and ran a sweep over the remaining 63 catch pairs in fd06bf2.

All catch (SQLException &ex) and catch (std::exception &ex) now read as catch (const SQLException &ex) / catch (const std::exception &ex) to match the style you set in DsnDialog.cpp.

@irodushka irodushka merged commit 35fae3e into FirebirdSQL:master May 15, 2026
10 checks passed
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.

2 participants