Hi Adriano!
I’m working on a full revamp of Firebird ODBC Driver and this library caught my eye. ❤️
May I ask you what's its current status? Are you planning to keep evolving it, or was it mainly a prototype?
I’d really like to use it in the ODBC driver; it could help us remove a lot of legacy code.
With a little help from Claude 😄 we jotted down a few topics we’d need to cover to move forward -- would you mind taking a look?
P.S. Of course, I'd be happy to contribute with PRs, as well.
Summary of Proposed Contributions
| # |
Feature |
PR Size |
Priority |
Notes |
Link |
Status |
| 2 |
Batch + BatchCompletionState classes |
Large (~500 LOC + tests) |
Critical |
We'll implement & test |
#26 |
✅ COMPLETED |
| 3 |
CursorType in StatementOptions |
Small (~20 LOC) |
High |
Trivial change |
#25 |
✅ COMPLETED |
| 8 |
Error vector in DatabaseException |
Medium (~50 LOC + tests) |
Critical |
Non-breaking addition |
#24 |
✅ COMPLETED |
| 6 |
Move assignment for Statement + Attachment |
Small (~30 LOC) |
Medium |
Enables container use |
#36 |
✅ COMPLETED |
| 5 |
alias field in Descriptor |
Small (~10 LOC) |
Medium |
One new field |
#37 |
✅ COMPLETED |
| 7 |
Thread safety docs in README |
Docs only |
Low |
After code PRs |
|
|
| 10 |
ODBC-style example |
Docs only |
Low |
After batch PR |
|
|
📋 Suggestions for Improvement
1. Missing IResultSet Wrapper / Multi-Row Fetch
Issue: The Statement class has execute() and fetchNext(), but there's no separate ResultSet abstraction. This makes it awkward to:
- Differentiate between a statement that returns results vs. one that doesn't
- Pass a result set to another function without passing the statement
- Support multiple simultaneous result sets from the same statement (e.g., stored procedures returning multiple cursors)
Suggestion: Consider adding a ResultSet class that wraps IResultSet and is returned from Statement::execute() when the statement produces a result set. This matches the JDBC pattern (Statement.executeQuery() returns ResultSet).
Alternative: If keeping the single-class design, document that Statement::execute() returns true if there are results to fetch, and fetchNext() should only be called after execute() returns true.
2. Batch Execution (IBatch) Not Exposed
Issue: The Firebird 4.0+ IBatch interface enables high-performance bulk inserts with a single server roundtrip. This is critical for ETL workloads and ODBC array parameter binding. The current API requires calling execute() N times for N rows.
Suggestion: Add a Batch class or extend Statement with:
void Statement::addBatch(); // Queue current parameter values
BatchResult Statement::executeBatch(); // Execute all queued rows in one roundtrip
The BatchResult could expose per-row status (IBatchCompletionState::getState()).
Priority: High — this is the single biggest performance differentiator between naive row-by-row and professional-grade bulk operations.
3. No Scrollable Cursor Support
Issue: Statement only exposes fetchNext(), fetchPrior(), fetchFirst(), fetchLast(), fetchAbsolute(), fetchRelative(). However, these require opening the result set with IStatement::openCursor() specifying CURSOR_TYPE_SCROLLABLE. The current implementation uses CURSOR_TYPE_SCROLLABLE (line 167 of Statement.cpp), but this is always enabled even for forward-only queries.
Suggestion: Add a CursorType enum to StatementOptions:
enum class CursorType { FORWARD_ONLY, SCROLLABLE };
- Default to
FORWARD_ONLY for performance (no server-side cursor buffering)
- When
SCROLLABLE, call openCursor() with CURSOR_TYPE_SCROLLABLE
Note: We observed that execute() always passes IStatement::CURSOR_TYPE_SCROLLABLE. For forward-only streaming of large result sets, this forces the server to buffer all rows. Consider defaulting to 0 (forward-only) and only using CURSOR_TYPE_SCROLLABLE when scrollable methods are needed.
5. Missing Descriptor Metadata Access
Issue: The Descriptor struct in Descriptor.h contains rich metadata (type, scale, length, nullability, field/relation names). However:
Statement::getInputDescriptors() and getOutputDescriptors() return const std::vector<Descriptor>&
- The
Descriptor struct is only defined for read access — applications can't modify it for deferred binding
Suggestion: For ODBC drivers and similar adapters, we need:
- Access to
IMessageMetadata::getField(), getRelation(), getAlias() etc. — currently these are processed internally but not fully exposed
- Ability to override input metadata (e.g., bind a string value as VARCHAR when the server expects INTEGER) — this requires
IMetadataBuilder
Consider exposing:
Statement::getInputMetadata() → FbRef<IMessageMetadata> (already exists!)
Statement::getOutputMetadata() → FbRef<IMessageMetadata> (already exists!)
- A helper to build custom
IMessageMetadata for input override scenarios
6. Statement Copy/Move Semantics
Issue: Statement is move-only (copy deleted, move-only). After a move, isValid() returns false. This is correct, but:
Statement::operator=(Statement&&) is deleted — why? This prevents std::swap and vector/map usage
- The moved-from statement's
statementHandle is moved but attachment reference becomes dangling
Suggestion: Either:
- Make
Statement fully movable (implement operator=(Statement&&))
- Or document explicitly that
Statement should be heap-allocated and managed via std::unique_ptr
7. Thread Safety Documentation
Issue: The library doesn't document thread safety guarantees. Questions:
- Is it safe to use one
Client from multiple threads?
- Is it safe to use one
Attachment from multiple threads with different Transactions?
- Is
StatusWrapper thread-safe (it contains a dirty flag)?
Suggestion: Add a "Thread Safety" section to the documentation. Based on our reading of the code:
Client appears thread-safe (only reads from master)
Attachment is NOT thread-safe (shared Transaction state)
Statement is NOT thread-safe (mutable inMessage/outMessage)
8. Error Message Localization
Issue: DatabaseException::what() returns the Firebird error message, which is localized based on the server's LC_MESSAGES setting. For ODBC drivers, we need access to:
- The raw ISC error vector (
intptr_t*) for SQLSTATE mapping
- The SQLCODE (via
IUtil::formatStatus() or equivalent)
Suggestion: Expose the raw error data in DatabaseException:
const std::vector<intptr_t>& getErrorVector() const;
int getSqlCode() const; // Computed via IStatus::getErrors()
9. Missing IUtil Wrapper
Issue: Firebird's IUtil provides valuable utilities:
formatStatus() — format error messages
decodeDate()/encodeDate() — manual date conversion
getClientVersion() — client library version
getInt128()/getDecFloat16()/getDecFloat34() — type converters
Some of these are used internally (NumericConverter, CalendarConverter) but not exposed.
Suggestion: Consider exposing Client::getUtil() → fb::IUtil* or wrapping the most useful methods.
Hi Adriano!
I’m working on a full revamp of Firebird ODBC Driver and this library caught my eye. ❤️
May I ask you what's its current status? Are you planning to keep evolving it, or was it mainly a prototype?
I’d really like to use it in the ODBC driver; it could help us remove a lot of legacy code.
With a little help from Claude 😄 we jotted down a few topics we’d need to cover to move forward -- would you mind taking a look?
P.S. Of course, I'd be happy to contribute with PRs, as well.
Summary of Proposed Contributions
Batch+BatchCompletionStateclassesCursorTypeinStatementOptionsDatabaseExceptionStatement+Attachmentaliasfield inDescriptor📋 Suggestions for Improvement
1. Missing
IResultSetWrapper / Multi-Row FetchIssue: The
Statementclass hasexecute()andfetchNext(), but there's no separateResultSetabstraction. This makes it awkward to:Suggestion: Consider adding a
ResultSetclass that wrapsIResultSetand is returned fromStatement::execute()when the statement produces a result set. This matches the JDBC pattern (Statement.executeQuery()returnsResultSet).Alternative: If keeping the single-class design, document that
Statement::execute()returnstrueif there are results to fetch, andfetchNext()should only be called afterexecute()returnstrue.2. Batch Execution (
IBatch) Not ExposedIssue: The Firebird 4.0+
IBatchinterface enables high-performance bulk inserts with a single server roundtrip. This is critical for ETL workloads and ODBC array parameter binding. The current API requires callingexecute()N times for N rows.Suggestion: Add a
Batchclass or extendStatementwith:The
BatchResultcould expose per-row status (IBatchCompletionState::getState()).Priority: High — this is the single biggest performance differentiator between naive row-by-row and professional-grade bulk operations.
3. No Scrollable Cursor Support
Issue:
Statementonly exposesfetchNext(),fetchPrior(),fetchFirst(),fetchLast(),fetchAbsolute(),fetchRelative(). However, these require opening the result set withIStatement::openCursor()specifyingCURSOR_TYPE_SCROLLABLE. The current implementation usesCURSOR_TYPE_SCROLLABLE(line 167 of Statement.cpp), but this is always enabled even for forward-only queries.Suggestion: Add a
CursorTypeenum toStatementOptions:FORWARD_ONLYfor performance (no server-side cursor buffering)SCROLLABLE, callopenCursor()withCURSOR_TYPE_SCROLLABLENote: We observed that
execute()always passesIStatement::CURSOR_TYPE_SCROLLABLE. For forward-only streaming of large result sets, this forces the server to buffer all rows. Consider defaulting to0(forward-only) and only usingCURSOR_TYPE_SCROLLABLEwhen scrollable methods are needed.5. Missing
DescriptorMetadata AccessIssue: The
Descriptorstruct inDescriptor.hcontains rich metadata (type, scale, length, nullability, field/relation names). However:Statement::getInputDescriptors()andgetOutputDescriptors()returnconst std::vector<Descriptor>&Descriptorstruct is only defined for read access — applications can't modify it for deferred bindingSuggestion: For ODBC drivers and similar adapters, we need:
IMessageMetadata::getField(),getRelation(),getAlias()etc. — currently these are processed internally but not fully exposedIMetadataBuilderConsider exposing:
Statement::getInputMetadata()→FbRef<IMessageMetadata>(already exists!)Statement::getOutputMetadata()→FbRef<IMessageMetadata>(already exists!)IMessageMetadatafor input override scenarios6.
StatementCopy/Move SemanticsIssue:
Statementis move-only (copy deleted, move-only). After a move,isValid()returnsfalse. This is correct, but:Statement::operator=(Statement&&)is deleted — why? This preventsstd::swapand vector/map usagestatementHandleis moved butattachmentreference becomes danglingSuggestion: Either:
Statementfully movable (implementoperator=(Statement&&))Statementshould be heap-allocated and managed viastd::unique_ptr7. Thread Safety Documentation
Issue: The library doesn't document thread safety guarantees. Questions:
Clientfrom multiple threads?Attachmentfrom multiple threads with differentTransactions?StatusWrapperthread-safe (it contains adirtyflag)?Suggestion: Add a "Thread Safety" section to the documentation. Based on our reading of the code:
Clientappears thread-safe (only reads frommaster)Attachmentis NOT thread-safe (sharedTransactionstate)Statementis NOT thread-safe (mutableinMessage/outMessage)8. Error Message Localization
Issue:
DatabaseException::what()returns the Firebird error message, which is localized based on the server'sLC_MESSAGESsetting. For ODBC drivers, we need access to:intptr_t*) for SQLSTATE mappingIUtil::formatStatus()or equivalent)Suggestion: Expose the raw error data in
DatabaseException:9. Missing
IUtilWrapperIssue: Firebird's
IUtilprovides valuable utilities:formatStatus()— format error messagesdecodeDate()/encodeDate()— manual date conversiongetClientVersion()— client library versiongetInt128()/getDecFloat16()/getDecFloat34()— type convertersSome of these are used internally (
NumericConverter,CalendarConverter) but not exposed.Suggestion: Consider exposing
Client::getUtil()→fb::IUtil*or wrapping the most useful methods.