Skip to content

v5 - Firebird ODBC Driver Modernization (fb-cpp edition)#283

Draft
fdcastel wants to merge 115 commits intoFirebirdSQL:masterfrom
fdcastel:v5-pr
Draft

v5 - Firebird ODBC Driver Modernization (fb-cpp edition)#283
fdcastel wants to merge 115 commits intoFirebirdSQL:masterfrom
fdcastel:v5-pr

Conversation

@fdcastel
Copy link
Copy Markdown
Member

@fdcastel fdcastel commented Apr 5, 2026

This is a revised version of the original v4 PR (#275), submitted two months ago.

A preview build is available at: https://github.com/fdcastel/firebird-odbc-driver/releases/tag/v5.0.0-preview

Note on items removed in v4: This version (v5) still does not restore the features that v4 removed — specifically: ODBC escape sequences ({fn}, {d}, {ts}, {oj}), the Windows ODBC Administrator UI forms (OdbcJdbcSetup). These are acknowledged mistakes from v4 and are tracked as future work. They are not part of this PR.

⚠️ DOT NOT MERGE - RFC ONLY ⚠️


Summary

This PR is a comprehensive modernization of the Firebird ODBC driver. It addresses crash bugs, ODBC specification violations, Unicode data corruption on Linux/macOS, and significant performance bottlenecks. The build system has been replaced with CMake + vcpkg, a 401-test Google Test suite has been added, and the source tree has been reorganized.

Compared to the original v4 PR, v5 adds:

  • Phase 9: Full Firebird OO API alignment — IBatch for array execution (single server roundtrip on FB4+), OO API used everywhere
  • Phase 10: Performance engineering — 10.88 ns/row fetch throughput (embedded Firebird), SRWLOCK replacing kernel mutex, 64-row prefetch buffer, LTO, stack-buffer Unicode path
  • Phase 11: SQL_ATTR_QUERY_TIMEOUT actually works (via IAttachment::cancelOperation()), SQLCancel actually cancels, SQLGetTypeInfo thread-safety and spec compliance, connection reset improvements
  • Phase 12: Native UTF-16 metadata caching (OdbcString), single UTF-8↔UTF-16 codec, eliminated W→A→W roundtrip for 7 metadata/diagnostic W-API functions
  • Phase 13: ~3,750 lines of dead code removed; test suite reorganized from phase-named to topic-named files; 401 unique tests
  • Phase 14: fb-cpp adoption — vcpkg manages all dependencies; IscDbc/ renamed to core/; JString replaced with std::string; RAII throughout (fbcpp::Attachment, fbcpp::Transaction, fbcpp::Statement, fbcpp::Blob, fbcpp::EventListener); ~2,600 lines of legacy utility code removed

v4 vs v5

v4 (original PR, Feb 2026) v5 (this PR, Apr 2026)
Build system CMake 3.20+ CMake 3.20+ + vcpkg
Dependencies FetchContent (GTest, Firebird headers) vcpkg (fb-cpp, GTest, Google Benchmark, Boost)
Database layer Hand-rolled IscDbc (~15,000 lines, 110 files) fb-cpp RAII wrappers + IscDbc core
String types Legacy JString std::string throughout
Directory src/IscDbc/ src/core/
Query timeout No-op Functional (IAttachment::cancelOperation())
SQLCancel No-op Functional (cancels in-flight statements)
W-API metadata W→A→W roundtrip + heap allocs Cached UTF-16, zero conversion
ODBC escapes ❌ Removed (acknowledged mistake) ❌ Still not restored (future work)
Windows ODBC Admin UI ❌ Removed (acknowledged mistake) ❌ Still not restored (future work)
ATL/DTC ❌ Removed ❌ Still removed

Motivation

The current driver (v3, official) has a solid foundation — it correctly uses the Firebird OO API for connections, transactions, statements, result sets, and blobs. But it accumulated technical debt over its 20+ year history, and several areas needed significant work: Unicode handling, error mapping, safety at the API boundary, performance, and build system modernization.

After the v4 PR was submitted in February, review feedback identified two mistakes: ODBC escape sequences are required by the ODBC spec, and the Windows ODBC Administrator UI should be preserved. Both points are correct. Those items are not in this PR and are deferred.

The v5 work continued in other areas — specifically performance, correctness of less-used API surface, and a significant architectural improvement: adopting the fb-cpp library by Adriano dos Santos Fernandes (Firebird core developer).


What Changed Since v4

The detailed issue tracking and rationale for every change is in Docs/FIREBIRD_ODBC_MASTER_PLAN.original.md.

Performance (Phase 10)

  • Mutex → SRWLOCK: Replaced Win32 CreateMutex/WaitForSingleObject/ReleaseMutex with SRWLOCK for the fetch hot path. Cost drops from ~1–2μs per lock/unlock to ~20ns uncontended.
  • 64-row prefetch buffer: IscResultSet::nextFetch() now fills a 64-row buffer via batched IResultSet::fetchNext() calls, then serves rows from the buffer. Amortizes per-call overhead across 64 rows.
  • Stack-buffer Unicode path: ConvertingString uses a 512-byte stack buffer before heap-allocating, eliminating virtually all W-API heap allocations.
  • LTO enabled: Release builds use link-time optimization for cross-TU inlining of OdbcConvert::conv* methods.
  • ODBC_FORCEINLINE on hot functions: Applied to 6 inner functions on the fetch data path.
  • Measured: 10.88 ns/row for 10K×10 INT columns (embedded Firebird, Release build).

Functional Fixes (Phase 11)

  • SQL_ATTR_QUERY_TIMEOUT: Now functional. When set, a background thread fires IAttachment::cancelOperation(fb_cancel_raise) after the timeout. Returns SQLSTATE HYT00. Value of 0 = no timeout (default).
  • SQLCancel: Now calls IAttachment::cancelOperation(fb_cancel_raise) on the current connection. Actually cancels in-flight statements.
  • SQLGetTypeInfo thread-safety: The static alphaV[] array was modified in-place by each TypesResultSet constructor, causing a data race under concurrent calls. Now copies to a per-instance std::vector before mutation.
  • SQLGetTypeInfo result ordering: Result set now sorted by DATA_TYPE ascending, as required by the ODBC spec.
  • SQL_ASYNC_MODE: Was incorrectly reporting SQL_AM_STATEMENT while rejecting SQL_ASYNC_ENABLE_ON with HYC00. Now correctly reports SQL_AM_NONE.
  • Connection reset: SQL_ATTR_RESET_CONNECTION now rolls back pending transactions and closes open cursors before returning a connection to the pool.

Unicode Path (Phase 12)

  • Single UTF-8↔UTF-16 codec: Utf16Convert.h/cpp is now the sole implementation. utf8_mbstowcs/utf8_wcstombs in MultibyteConvert.cpp were redundant and targeting wchar_t (4 bytes on Linux — wrong).
  • SQLWCHAR* throughout: All *ToStringW conversion functions now use SQLWCHAR* instead of wchar_t*. This fixes silent data corruption on Linux/macOS where wchar_t is 4 bytes.
  • Native UTF-16 metadata cache (OdbcString): A new OdbcString class stores column/table names as cached UTF-16 strings in DescRecord (11 fields). SQLDescribeColW, SQLColAttributeW, SQLGetDescFieldW, SQLGetDescRecW, SQLGetDiagRecW, SQLGetDiagFieldW all read from this cache — zero encoding conversion on the output path.
  • Default CHARSET=UTF8: When CHARSET is not specified in the connection string, the driver now defaults to UTF8. This ensures consistent encoding and correct server-side transliteration. The previous behavior (defaulting to CHARSET=NONE) caused incorrect results on non-UTF-8 locales.

fb-cpp Adoption (Phase 14)

The biggest structural change in v5 is adoption of fb-cpp (v0.0.4), now managed via vcpkg.

What fb-cpp replaced:

  • CFbDll / LoadFbClientDll — ~600 lines → FbClient singleton using fbcpp::FbApiHandle (~50 lines)
  • InfoTransaction + TPB byte-stuffing — ~100 lines → fbcpp::Transaction + fbcpp::TransactionOptions
  • IAttachment lifecycle management → fbcpp::Attachment (RAII)
  • IStatement lifecycle management → fbcpp::Statement (RAII)
  • IBlob lifecycle + manual segment I/O → fbcpp::Blob (RAII)
  • IscUserEvents event buffer management → fbcpp::EventListener
  • JString custom string class → std::string
  • DateTime.h/.cpp, SqlTime.h/.cpp, TimeStamp.h/.cpp — consolidated into FbDateConvert.h POD structs

What did NOT change (the IscDbc "core" layer is not being deleted — only modernized):

  • IscConnection, IscStatement, IscResultSet, IscPreparedStatement — still exist, now wrapping fb-cpp objects
  • Sqlda, OdbcConvert, OdbcStatement, OdbcConnection — unchanged in structure
  • Value/BinaryBlob/Stream — retained as the driver's internal data representation layer

Net code reduction: ~2,600 lines removed (479 added, 2,604+ deleted) in Phase 14 alone.

src/IscDbc/src/core/: The IscDbc directory has been renamed to core/ to better reflect its role. It no longer contains legacy JDBC-like code — it contains the driver's core database interaction layer.

Dead Code Removal (Phase 13)

  • Deleted 14 dead source files: LinkedList.cpp/h, Lock.cpp/h, ServiceManager.cpp/h, SupportFunctions.cpp/h, OdbcDateTime.cpp/h, Engine.h, WriteBuildNo.h, IscDbc.def/.exp, OdbcJdbcMinGw.def, OdbcJdbc.dll.manifest, makefile.in (×2)
  • Removed BUILD_SETUP CMake option (references non-existent OdbcJdbcSetup)
  • Test suite reorganized: phase-named files (test_phase7_crusher_fixes.cpp, test_phase11_typeinfo_timeout_pool.cpp) redistributed to topic-named files; duplicate tests removed
  • 401 unique tests remain after consolidation (down from 432 which included ~31 duplicates)

Testing

401 tests, all passing on Windows x64, Linux x64, Linux ARM64 via CI.

Test files are organized by topic:

File Tests Topic
test_null_handles.cpp 65 API boundary safety (null/invalid handles)
test_connect_options.cpp ~30 Connection attributes, timeouts, async, pool reset
test_connection_attrs.cpp ~10 Connection attribute persistence
test_catalogfunctions.cpp 22 All 12 catalog functions + type info ordering
test_data_types.cpp ~16 Data type round-trips
test_result_conversions.cpp 35 Result column type conversions
test_param_conversions.cpp 18 Parameter type conversions
test_array_binding.cpp 17 Column-wise + row-wise array parameter binding
test_blob.cpp ~5 BLOB read/write/NULL
test_descrec.cpp 10 SQLGetDescRec/SQLDescribeCol for all types
test_descriptor.cpp ~10 SQLGetDescRec, SQLSetDescRec, SQLCopyDesc
test_errors.cpp ~15 Error handling, SQLSTATE, diagnostics, row count
test_escape_sequences.cpp ~5 ODBC escape sequence passthrough
test_cursor_name.cpp 9 SQLSetCursorName/SQLGetCursorName
test_cursors.cpp ~7 Scrollable cursors, commit/rollback
test_cursor_commit.cpp 6 Cursor behavior across commit/rollback
test_scrollable_cursor.cpp 9 All fetch orientations (FIRST, LAST, PRIOR, etc.)
test_data_at_execution.cpp 6 SQL_DATA_AT_EXEC / SQLPutData
test_bindcol.cpp 5 Bind/unbind cycling
test_guid_and_binary.cpp 14 SQL_GUID, UUID, BINARY/VARBINARY, FB4+ types
test_odbc38_compliance.cpp 12 ODBC 3.8 spec compliance
... ... ...

Micro-benchmarks in tests/bench_fetch.cpp (Google Benchmark):

Benchmark Result
Fetch 10K rows × 10 INT columns (embedded FB) 10.88 ns/row
Fetch 10K rows × 5 VARCHAR(100) columns 10.60 ns/row
Fetch 1K rows × 1 BLOB column 74.1 ns/row

Breaking Changes

  • Build system: CMake is now required. Legacy build configurations (MSVC .sln, makefiles, MinGW, Borland) are removed.
  • vcpkg required: The build now uses vcpkg to manage dependencies. Run vcpkg install (or let CMake handle it via the toolchain file) before configuring.
  • Vendored headers: Firebird headers and ODBC SDK headers are no longer in the repository. vcpkg provides them at build time.
  • Source layout: Source files in src/. IscDbc layer in src/core/ (was src/IscDbc/).
  • CHARSET default: When not specified in the connection string, CHARSET now defaults to UTF8 (was NONE). This is a correctness improvement — CHARSET=NONE with locale-dependent mbstowcs was producing incorrect results on non-UTF-8 locales.
  • ODBC escape sequences: {fn}, {d}, {ts}, {oj} escape processing has been removed (from v4 — not restored in v5). Applications using ODBC escapes will not work correctly.
  • ATL/DTC: Distributed transaction support (ATL COM) has been removed.

Transparency Note

This work was substantially assisted by AI (LLMs). I believe in being upfront about this: the AI was used for code generation, analysis, testing, and documentation throughout this project. Every change was reviewed, tested against a real Firebird database, and verified on CI.

- Uses the tag version as a suffix in the filenames of published artifacts.
- Consolidate Windows jobs with `strategy.matrix`.
- Add ARM64 support to release workflow
- Adds debug .zip files.
- Removes MakePackage.bat and sed package.
- Removes unused (commented) sections from OdbcJdbcSetup.iss.
- Convert project to CMake (C++17) for Windows and Linux
- Add comprehensive Google Test integration
- Create GitHub Actions workflows for build/test and releases
- Include PSFirebird-based database setup for testing
- Add ATL stubs for building without Windows transaction support
- Create documentation and helper scripts
- Connection tests validate ODBC functionality

Resolves CMake multiplatform requirements
…Linux

- Use PowerShell 7 (pwsh) instead of Windows PowerShell 5.1 for PSFirebird compatibility
- Create Linux database in /var/lib/firebird/data with proper permissions
- Run isql-fb as firebird user to avoid permission errors
- PSFirebird provides complete Firebird environment in /fbodbc-tests/fb502
- Use embedded mode via CLIENT parameter in connection string
- Install PowerShell on Linux to use PSFirebird
- Remove Firebird server installation (not needed for embedded mode)
- Update documentation to reflect embedded approach
Tasks 1.1 and 1.2 from FIREBIRD_ODBC_MASTER_PLAN.md:

New files:
- OdbcSqlState.h: Master SQLSTATE table with 121 entries, each carrying
  both ODBC 3.x and 2.x SQLSTATE strings. Includes 100+ ISC error code
  mappings and 130+ SQL error code mappings. Replaces the sparse 19-entry
  ISC table and 3-entry SQL code table.
- Tests/Cases/SqlStateMappingTests.cpp: 16 new integration tests covering
  syntax errors, table/column not found, constraint violations, numeric
  overflow, conversion errors, connection errors, login failures, FK
  violations, division by zero, table-already-exists, and ODBC 2.x
  SQLSTATE version mapping (37000, S0002).

Modified files:
- OdbcError.h: Added sqlStateIndex member and getVersionedSqlState() method
- OdbcError.cpp: Constructors now use OdbcSqlState.h lookup functions with
  ISC code → SQL code → default state priority chain. sqlGetDiagRec and
  sqlGetDiagField return version-appropriate SQLSTATEs based on
  SQL_ATTR_ODBC_VERSION setting.
- OdbcTests.vcxproj: Added SqlStateMappingTests.cpp

Resolves: H-2 (SQLExecDirect HY000 for syntax errors)
Resolves: H-3 (Incomplete ISC→SQLSTATE mapping)
Resolves: H-15 (No ODBC 2.x/3.x dual SQLSTATE mapping)
Driver fixes:
- SQLGetDiagRec: return SQL_NO_DATA (ODBC 3.x) instead of SQL_NO_DATA_FOUND (H-9)
- SQLGetDiagField: add NULL ptr checks, fix duplicate SQL_DIAG_NUMBER, fix counter (H-10)
- SQLGetEnvAttr: return actual useAppOdbcVersion for SQL_ATTR_ODBC_VERSION (H-4)
- SQLSetConnectAttr: add default error path returning HY092 (H-5)
- SQLGetConnectAttr: fix StringLengthPtr passthrough (H-6)
- SQLGetInfo: add NULL ptr checks, fix infoLong to always use SQLUINTEGER size (H-7/H-13)
- SQL_SCHEMA_USAGE: fix to use supportsSchemasInIndexDefinitions (H-8)
- SQLCloseCursor: return 24000 when no cursor is open (H-1)
- SQLSetStmtAttr: add cursor-state validation for 24000 (H-11)
- SQLGetInfoW: validate even BufferLength for string InfoTypes (H-12)
- SQLDescribeColW: map SQL_CHAR->SQL_WCHAR, SQL_VARCHAR->SQL_WVARCHAR (H-14)
- SQLGetStmtAttr: add SQL_ATTR_CURSOR_SCROLLABLE case

Test fixes:
- InfoTests: use SQLWCHAR buffers with SQLGetInfoW (T-2)
- Disable crash tests (C-2 GUARD_* bugs) with skip messages
- Fix CursorScrollable to handle DM-managed attribute
- Fix BufferLength_Even to handle DM interception

Infrastructure:
- run.ps1: cross-platform (Windows MSBuild+VSTest, Linux CMake+CTest)
- test.yml: add Linux build deps, PSFirebird on all platforms
- build-and-test.yml: Linux uses PSFirebird instead of manual Firebird download

All 84 tests pass on Windows
…gister driver

- Add Utf16Convert.cpp to CMakeLists.txt (fixes Windows CMake linker errors)
- Fix run.ps1 nproc handling on Linux (was passing empty -j arg)
- test.yml: register ODBC driver on Windows before testing
- test.yml: separate Build and Test steps for proper driver registration
- Fix wchar_t/SQLWCHAR type mismatch in OdbcConvert.cpp GET_WLEN_FROM_OCTETLENGTHPTR macro (cast to const SQLWCHAR*)
- Add libncurses5/libtinfo5 compatibility for Firebird on newer Ubuntu
- Create symlink fallback when libncurses5 package is unavailable
… Firebird DB setup

- Link libodbcinst on Linux (fixes SQLGetPrivateProfileString undefined reference)
- Add Ubuntu 22.04 jammy repo for libncurses5/libtinfo5 packages (PSFirebird dependency)
- Add 'Create Firebird Test Database' step to test.yml
- Use ports.ubuntu.com for ARM64 runners
- Make run.ps1 Install-OdbcDriver-Linux resilient to missing odbcinst
- Fall back to writing odbcinst.ini directly if odbcinst not found
- Fix build-and-test.yml verification step to not fail on missing odbcinst
- Install odbcinst package explicitly in CI workflows
- All 84 tests pass (100% pass rate) on Windows, Linux x64, Linux ARM64
- CI fully operational: test.yml and build-and-test.yml both green
- SQLSTATE mapping coverage now 90%+ (121 kSqlStates, 100+ ISC mappings)
- Unicode compliance at 100%
- Updated test infrastructure section (T-6 CI status)
- Document version bumped to 1.1
- Fix GUARD_* macros to check null before dereference (C-1, C-2)
  - Added NULL_CHECK macro to all GUARD_* variants in Main.h
  - Returns SQL_INVALID_HANDLE before any handle dereference

- Add null checks at all ODBC entry points (C-3)
  - SQLCancel, SQLFreeEnv, SQLDisconnect, SQLGetEnvAttr, SQLSetEnvAttr
  - SQLFreeHandle, SQLAllocHandle, SQLCopyDesc
  - Fixed SQLCopyDesc null check ordering (was after GUARD_HDESC)

- Fix sprintf buffer overflow in debug builds (C-6)
  - Replaced sprintf with snprintf in OdbcConnection.cpp
  - Increased debug buffer from 256 to 512 bytes

- Replace 64 C-style exception casts across 12 files (C-7)
  - Changed catch(std::exception&) + (SQLException&) downcast
    to direct catch(SQLException&) - type-safe, no UB

- Add 90 null handle crash prevention tests (T-9)
  - 28 MSTest tests in Tests/Cases/NullHandleTests.cpp
  - 62 GTest tests in Tests/test_null_handles.cpp
  - Total tests: 112 (all passing)

- Update FIREBIRD_ODBC_MASTER_PLAN.md to v1.2
… odbc32 from driver

- Fix NullHandleTests to load driver DLL directly via exe-relative paths
  instead of going through ODBC Driver Manager (which was picking up
  system-installed C:\Windows\SYSTEM32\FirebirdODBC.dll instead of built DLL)
- Add post-build step to copy driver DLL next to test executable
- Set CTest PATH environment for GTest/driver DLL discovery
- Change connection integration tests to GTEST_SKIP when
  FIREBIRD_ODBC_CONNECTION env var is not set (100% CTest pass rate)
- Remove odbc32.dll from driver link libraries (driver provides its own
  ODBC entry points via .def file, should not import from Driver Manager)
- Restore Utf16Convert.cpp/h files (were missing from repo)
- Update FIREBIRD_ODBC_MASTER_PLAN.md with T-13, T-14 fixes

Issues: T-13 (system DLL loading), T-14 (connection test failures)
…th ATL

- Fix SQLColAttribute to use SQLLEN* for numericAttribute on UNIX platforms
  (matching unixODBC header declaration). Previously only used SQLLEN* on
  _WIN64, causing symbol mismatch on Linux (T-13 related).
- Fix DllMainSetup linker error when ATL is available: always compile
  AtlStubs.cpp but guard ATL-specific stubs with HAVE_ATL preprocessor
  define. DllMainSetup is now unconditionally available.
- Skip dlclose in Linux test teardown to prevent 'double free' crash
  during process exit when driver global destructors conflict with test
  process cleanup.
…ux exit crash

- Move #include <windows.h> before #ifndef HAVE_ATL in AtlStubs.cpp so
  BOOL/APIENTRY/HINSTANCE types are available for DllMainSetup
- Use _Exit() on Linux to skip driver's EnvShare global destructor
  that causes 'double free or corruption' during process exit (L-8)
- Use RTLD_NODELETE when loading driver .so to keep it mapped
Remove Microsoft Distributed Transaction Coordinator (DTC) support and
its ATL (Active Template Library) dependency. This was Windows-only
complexity that required ATL headers to build, provided no value for
Firebird use cases, and caused CI build issues.

Deleted files:
- AtlStubs.cpp (ATL stub implementations)
- ResourceManagerSink.cpp/h (DTC COM callback)
- TransactionResourceAsync.cpp/h (DTC transaction enlistment)

Changes:
- CMakeLists.txt: Remove ATL detection, HAVE_ATL, conditional source
  lists. SafeEnvThread.cpp now compiled unconditionally (fixes a bug
  where the global DLL mutex was never initialized in non-ATL builds).
- Main.cpp: Remove clearAtlResource(), DllMainSetup() from DllMain
- OdbcConnection.h: Remove IsInstalledMsTdsInterface(),
  enlistTransaction(), enlistConnect member
- OdbcConnection.cpp: Remove SQL_ENLIST_IN_DTC attribute handling,
  enlistConnect initialization

Thread safety (SafeEnvThread.h, SafeDllThread, SafeConnectThread) is
preserved — these are general-purpose ODBC thread guards, not ATL.

Closes M-6 as WONTFIX.
…se 2)

Entry Point Hardening (Phase 2 of roadmap):

Task 2.1 + 2.2: Consistent entry-point pattern
- Added clearErrors() to sqlPutData and sqlSetPos (were missing it)
- Added try/catch to 9 inner methods that lacked exception handling:
  - OdbcStatement: sqlPutData, sqlSetPos, sqlFetch, sqlGetData
  - OdbcDesc: sqlSetDescField
  - OdbcConnection: sqlGetConnectAttr, sqlGetInfo, sqlSetConnectAttr,
    sqlGetFunctions
- sqlGetData: replaced partial inner try/catch with full-method coverage
- All ODBC entry points now follow the same disciplined pattern:
  clearErrors -> try { ... } catch (SQLException&) { postError; return ERROR; }

Task 2.4: Remove DRIVER_LOCKED_LEVEL_NONE
- Removed DRIVER_LOCKED_LEVEL_NONE (level 0) from OdbcJdbc.h
- Removed no-locking fallback branch from Main.h GUARD macros
- Added compile-time #error guard to prevent unsafe configuration
- Thread safety is now always enabled (ENV or CONNECT level)
…d README

Phase 2 completion:
- 2.3: Statement-level savepoint isolation (M-1 resolved)
  - Added setSavepoint/releaseSavepoint/rollbackSavepoint to Connection interface
  - Implemented in IscConnection using Firebird IAttachment::execute()
  - Wrapped IscStatement::execute() and executeProcedure() with savepoint
    isolation when autoCommit=OFF — failed statements no longer corrupt
    the transaction

Phase 3 completion (131 tests total, 129 pass, 2 skip):
- test_helpers.h: Shared OdbcConnectedTest fixture with RAII TempTable
- test_cursor.cpp: Cursor names, block fetch, close/re-exec, describe col
- test_descriptor.cpp: IRD/ARD/APD/IPD, SQLGetDescField, SQLCopyDesc
- test_multi_statement.cpp: Interleaved statements, 20 simultaneous handles
- test_data_types.cpp: 18 tests covering all SQL types, conversions, params
- test_blob.cpp: Small/large (64KB) text BLOBs, NULL BLOBs
- test_bind_cycle.cpp: Rebind, unbind, reset params, prepare/execute cycling
- test_savepoint.cpp: PK violation isolation, multiple failures, rollback
- test_catalog.cpp: SQLTables, SQLColumns, SQLPrimaryKeys, SQLGetTypeInfo,
  SQLStatistics, SQLSpecialColumns
- test_escape_sequences.cpp: {d}, {ts}, {fn}, {oj} escapes, SQLNativeSql

Documentation:
- README.md: Full project documentation (connection params, build, test, install)
- AGENTS.md: Moved database connection info to RULE #0.1 for visibility
- Master plan updated to v1.6
fdcastel added 26 commits March 10, 2026 15:28
…t RAII

Replace raw Firebird::IStatement* lifecycle management in IscStatement
with fbcpp::Statement RAII wrapper:

- IscStatement now stores std::unique_ptr<fbcpp::Statement> fbStatement_
- prepareStatement() creates fbcpp::Statement when connection has fb-cpp
  transaction (dialect 3, non-shared connection). Falls back to raw API
  for shared/DTC connections and legacy dialect 1.
- statementHandle kept as cached raw pointer from
  fbStatement_->getStatementHandle() for efficient access by existing
  execute/fetch code paths (unchanged).
- freeStatementHandle() delegates to fbStatement_.reset() when fb-cpp
  owns the handle, eliminating manual free/release calls.
- Execute, fetch, and Sqlda buffer paths remain unchanged for ODBC
  performance (zero-copy buffer access preserved).

Also updates master plan (v3.9): marks 14.4.1 as done, documents
blocking issues for 14.4.2-14.4.6 (fb-cpp buffer incompatibility,
missing fbcpp::Batch and CursorType contributions).
…g, and batch execution to fbcpp; delete legacy statement files
…e cursors (Phase 14.4.4, 14.4.5, 14.4.7.6)

- Upgrade fb-cpp from v0.0.2 to v0.0.4 via vcpkg registry baseline update
- All previously blocked requisites now resolved: dialect support, getOutputMessage(),
  RowSet, Batch, CursorType, DatabaseException error vectors, move assignment,
  Descriptor::alias

Phase 14.4.4: Migrate batch execution from raw IBatch to fbcpp::Batch
- Replace IBatch* member with unique_ptr<fbcpp::Batch> for RAII cleanup
- Use fbcpp::BatchOptions (setRecordCounts, setMultiError, setBlobPolicy)
- Use fbcpp::BatchCompletionState for type-safe result processing
- Use fbcpp::BlobId for registerBlob() instead of raw ISC_QUAD
- Buffer assembly logic retained (ODBC conversion path unchanged)
- Net reduction: ~20 lines, eliminated manual IXpbBuilder/dispose calls

Phase 14.4.5: Migrate scrollable cursors
- Add setScrollable() to InternalStatement interface
- OdbcStatement propagates cursorScrollable flag before execute
- IscStatement::execute() passes CURSOR_TYPE_SCROLLABLE to openCursor()

Phase 14.4.7.6: Eliminate dialect fallback
- Pass connection dialect via StatementOptions::setDialect() to fbcpp::Statement
- Remove getDatabaseDialect() >= 3 guard from fbcpp path
- All dialects (1 and 3) now use fbcpp::Statement
- Raw API fallback remains only for shared/DTC connections

Update master plan: mark 14.4.4, 14.4.5, 14.4.7.6 complete; update fb-cpp
gaps section (all requisites resolved in v0.0.4)
- Upgrade actions/checkout v4 → v6
- Upgrade actions/cache v4 → v5
- Upgrade actions/upload-artifact v4 → v7
- Upgrade actions/download-artifact v4 → v8
- Add 'Update vcpkg' step to bootstrap latest vcpkg (parallel task support)
- Remove hardcoded VCPKG_ROOT env vars (auto-detected from VCPKG_INSTALLATION_ROOT)

Previous Linux failure was transient (ICU download HTTP 504 timeout).
The runner's vcpkg installation has local modifications (port patches).
Use 'git fetch + reset --hard' instead of 'git pull' to cleanly update.
…ng (14.4.7.1)

Phase 14.4.7.1 — Eliminate Sqlda input buffer:
- Add Sqlda::remapToExternalBuffer() to repoint sqlvar at external memory
- Add CAttrSqlVar::assignBuffer(char*, size_t) overload for raw pointers
- Add Sqlda::activeBufferData()/activeBufferSize() for buffer indirection
- In prepareStatement (fbcpp path), remap inputSqlda to fbcpp::inMessage
- OdbcConvert writes directly into fbcpp's buffer (zero-copy)
- execute()/executeProcedure()/batchAdd() use activeBufferData()
- checkAndRebuild() copies from external->execBuffer for type overrides
- Raw API fallback retains internal Sqlda::buffer

Phase 14.4.7.2 — Output buffer NOT remapped:
- IRD records cache dataPtr at defFromMetaDataOut() time; remapping
  would desynchronize them when static cursors call initStaticCursor()
- Output Sqlda::buffer remains the stable target for all fetch paths

Phase 14.4.7.3/14.4.7.4 — Analysis complete:
- RowSet migration deferred (fbcpp auto-fetches first row on execute)
- Metadata rebuild already compatible with external buffers

All 401 tests pass. Zero warnings.
…e IscCallableStatement)

Phase 14.4.7.2a: Add OdbcDesc::refreshIrdPointers()
- New method iterates IRD records and re-reads dataPtr/indicatorPtr
  from headSqlVarPtr->getSqlData()/getSqlInd()
- Keeps cached IRD pointers in sync after buffer remap or static
  cursor initialization

Phase 14.4.7.2b: Remap output sqlvar to fbcpp::outMessage
- outputSqlda.remapToExternalBuffer() called in prepareStatement()
  after allocBuffer(), eliminating duplicate output buffer
- IscResultSet::nextFetch(), next(), and all execute paths use
  activeBufferData() instead of buffer.data()
- Prefetch buffer sized from lengthBufferRows (not buffer.size())
- refreshIrdPointers() called at start of each fetch cycle
  (sqlFetch, sqlFetchScroll, sqlExtendedFetch NoneFetch init)

Phase 14.4.7.2c: Fix static cursor with external output buffer
- initStaticCursor() detects externalBuffer_ and re-allocates
  internal buffer, restores sqlvar pointers, clears external state
- OdbcStatement calls refreshIrdPointers() after readStaticCursor()

Phase 14.4.3: Closes automatically (output remap complete)

Phase 14.4.6a: Merge IscCallableStatement into IscPreparedStatement
- IscPreparedStatement now inherits CallableStatement (extends
  PreparedStatement) instead of PreparedStatement directly
- Moved: OUT-parameter methods, executeCallable(), prepareCall(),
  rewriteSql()/getToken(), charTable global + init
- IscConnection::prepareCall() creates IscPreparedStatement directly
- Deleted IscCallableStatement.h/.cpp (~300 lines removed)

All 401 tests pass.
…ons (Phase 14.4.7.5)

- 14.4.7.5a: Extract CDataStaticCursor to CDataStaticCursor.h/.cpp (~300 lines)
- 14.4.7.5b: Extract metadata queries to SqldaMetadata.h/.cpp, fold into IscStatementMetaData
- 14.4.7.5c: Extract CAttrSqlVar/SqlProperties/AlignedAllocator to CAttrSqlVar.h;
  convert all Sqlda methods to sqlda_*() free functions with inline wrapper forwarders
- 14.4.7.5d: Move checkAndRebuild() to sqlda_check_and_rebuild() free function

Sqlda is now a plain data struct. All logic lives in free functions.
177 call sites preserved via inline wrappers for backward compatibility.
All 401 tests pass.
…pp RAII wrappers

Phase 14.5 (Blob Migration):
- Replace raw Firebird::IBlob* in IscBlob with std::unique_ptr<fbcpp::Blob>
- fetchBlob() uses fbcpp::Blob(attachment, transaction, blobId) + readSegment()
- writeBlob/writeStreamHexToBlob use fbcpp::Blob(attachment, transaction) + writeSegment()
- directOpenBlob() uses fbcpp::Blob::getLength() instead of manual isc_info parsing
- directFetchBlob/directWriteBlob use fbcpp::Blob read()/writeSegment()
- directCloseBlob() simplified to unique_ptr::reset() (RAII)
- Removed manual close()/release() error handling patterns

Phase 14.6 (Events Migration):
- Replace manual OO API event handling in IscUserEvents with fbcpp::EventListener
- Remove FbEventCallback class, initEventBlock(), vaxInteger(), eventCounts()
- EventListener auto-re-queues; onEventFired() bridges to ParameterEvent counts
- Thread-safe via std::mutex for concurrent event count access

Phase 14.7-14.8 assessed and deferred:
- SQLException/SQLError hierarchy: 67 catch blocks, 100+ throw sites — too invasive
- DateTime/TimeStamp/SqlTime/Value/JString: deeply embedded in data pipeline
- Master plan updated with detailed deferral rationale

All 401 tests pass on Debug and Release builds.
…ers (14.7.1, 14.7.4a-c)

Phase 14.7.1: Add SQLError::fromDatabaseException() factory method that
preserves fbcpp::DatabaseException error codes and SQLSTATE. Updated ~20
catch sites in IscBlob, IscConnection, IscStatement, IscOdbcStatement,
IscUserEvents.

Phase 14.7.4a: Replace JString with std::string in IscConnection.h (9
fields), IscStatement.h (1), SQLError.h (2), EnvShare.h (1). Fix all
implicit const char* conversions, IsEmpty() to empty(), Format() to
snprintf.

Phase 14.7.4b: Replace JString with std::string in OdbcConnection.h (18
fields), OdbcStatement.h (2), OdbcError.h (1), OdbcObject.h (1). Change
appendString() to accept string_view. Fix readAttribute/readAttributeFileDSN
return types and all caller sites.

Phase 14.7.4c: Replace JString with std::string in DescRecord.h (11
fields). Fix getString() to c_str(), remove JString locals from
OdbcStatement.cpp and OdbcDesc.cpp.

All 401 tests pass. JString still retained for ConnectDialog (Windows-only)
and some IscDbc internal return types (getIscStatusText, getInfoString).
…lidation, dead code cleanup, IscDbc→core rename

Phase 14.7.1: Enrich error bridging
- Add SQLError::fromDatabaseException() factory preserving fbcode/sqlstate/text
- Update ~20 catch sites to use fromDatabaseException()

Phase 14.7.2: Replace date/time types
- Consolidate DateTime/SqlTime/TimeStamp into FbDateConvert.h as POD structs
- Move string conversion logic to FbDateConvert.cpp
- Delete DateTime.cpp/.h, SqlTime.cpp/.h, TimeStamp.cpp/.h (~400 lines removed)

Phase 14.7.4a-c: Complete JString→std::string migration
- Replace JString with std::string in IscConnection (9 fields), IscStatement (1),
  SQLError (2), EnvShare (1), OdbcConnection (18), OdbcStatement (2),
  OdbcError (1), DescRecord (13), ConnectDialog (3)
- Remove JString from FbClient.h, IscArray, IscColumnsResultSet, IscDbc.h
- Delete JString.cpp/.h

Phase 14.8.1: Delete dead code
- Remove Attachment.h/.cpp (dead since Phase 14.2)
- Remove Error.cpp, IscCallableStatement.cpp (not in CMakeLists.txt)

Phase 14.8.2: Rename src/IscDbc/ to src/core/
- Update all #include paths from IscDbc/ to core/
- Update CMakeLists.txt include_directories and add_subdirectory
- Keep CMake target name 'IscDbc' for backward compatibility

All 401 tests pass.
…nd Benchmark

- Add gtest and benchmark to vcpkg.json manifest (15.1.1)
- Replace FetchContent_Declare(googletest/benchmark) with find_package() (15.2.2, 15.2.3)
- Link against vcpkg namespaced targets GTest::gtest, GTest::gtest_main (15.2.5)
- Remove gtest/gmock/benchmark DLL copy steps (now static via vcpkg triplet)
- Document vcpkg setup in README (15.1.4)
- Update master plan task statuses and success criteria
…tr) is UB)

The JString→std::string migration in Phase 14.7 introduced a crash in
SQLGetTypeInfo and all catalog metadata paths. JString's constructor
handled nullptr gracefully (treating it as empty string), but
std::string(nullptr) is undefined behavior — causing SEGFAULT on Linux
and access violation (0xc0000005) on Windows.

Root cause: IscStatementMetaData methods (getColumnName, getTableName,
getColumnLabel, getSqlTypeName, getColumnTypeName) returned raw const
char* from CAttrSqlVar fields (sqlname, relname, aliasname) which are
nullptr for synthetic result sets like TypesResultSet. When
OdbcDesc::defFromMetaDataOut assigned these to std::string fields in
DescRecord, the nullptr triggered UB.

Fix: Add safe_str() null guard in IscStatementMetaData to return empty
string instead of nullptr. This is the system boundary where all
metadata const char* flows into the ODBC layer.

Affected: 16 tests (SQLGetTypeInfo path) — all TypeInfoTest,
GuidTest, Fb4PlusTest, CatalogFunctionsTest, ServerVersionTest.
…uto-bootstrap

- Add vcpkg 2026.03.18 (c3867e71) as git submodule for reproducible build toolchain
- Update vcpkg-configuration.json to use kind:builtin with 2026.03.18 baseline
  (eliminates redundant git clone of vcpkg registry during install)
- CMakeLists.txt: auto-detect and auto-bootstrap vcpkg submodule before project()
  (submodule takes priority over VCPKG_ROOT / VCPKG_INSTALLATION_ROOT env vars)
- CI workflow: cache vcpkg submodule per-platform to avoid re-cloning on each run
  (first run clones vcpkg once; subsequent runs restore from actions/cache)
- CI workflow: set VCPKG_ROOT=github.workspace/vcpkg for consistent toolchain path

2026.03.18 ships significant performance improvements via parallel port installation.
…coverage

Phase 16.2: Test infrastructure improvements
- 16.2.1: Remove duplicate tests (CopyDescCrashTest, DiagRowCountTest,
  TypeInfoTest, TruncationIndicatorTest, ConnectionTimeoutTest,
  AsyncEnableTest, AsyncModeTest, QueryTimeoutTest, ConnectionResetTest)
- 16.2.2: Rename phase-numbered files: test_phase7_crusher_fixes.cpp ->
  test_connection_attrs.cpp, test_phase11_typeinfo_timeout_pool.cpp ->
  test_query_timeout.cpp
- 16.2.3: Parameterize conversion tests with TEST_P() suites
  (ToStringParam, ToIntParam, ToDoubleParam, NullParam, CharParamCase)
- 16.2.4: Extract test constants to test_helpers.h (kDefaultBufferSize,
  kMaxVarcharLen, kSmallBufferSize, kStressRowCount, kLargeBlobSize,
  kGetDataChunkSize)
- 16.2.5: Rewrite test_connection.cpp to use OdbcConnectedTest + TempTable
- 16.2.6: Add ASSERT_ODBC_SUCCESS/EXPECT_ODBC_SUCCESS macros
- 16.2.7: Stabilize CancelFromAnotherThread with retry-with-backoff

Phase 16.3: Coverage expansion
- 16.3.1: BLOB edge cases (empty, boundary-size, binary round-trip)
- 16.3.2: Error recovery paths (constraint retry, cursor reuse, rapid cycles)
- 16.3.3: Concurrent connections (independent queries, isolation)
- 16.3.4: SQLGetInfo completeness (6 new info type tests)

All 418 tests pass.
…aring

Replace the monolithic per-workflow binary cache directory (stored as one
big zip keyed by vcpkg.json+vcpkg-configuration.json+.gitmodules hash) with
vcpkg's native x-gha provider.

Root cause of the 25-min build regression:
- Phase 15 added gtest/benchmark to vcpkg (57 packages total, ICU alone
  takes 8.9 min to compile)
- Phase 15.4 updated the vcpkg submodule, which changed .gitmodules
- Each change invalidated the ENTIRE binary cache for all 57 packages
- GitHub Actions cache is branch-scoped: feature branches could not reuse
  sibling-branch caches, so every new branch got a cold 25-min start

Why x-gha is better:
- Each package is cached individually with a content-addressable key
  (based on package hash, not workflow input files)
- All branches in the repo share the same package cache pool
- Partial upgrades: if ICU version doesn't change, its cache entry is
  still valid even when gtest is updated
- Simpler workflow: no more manual cache directory creation or
  monolithic actions/cache entries for packages
Three layered defences against tests that block indefinitely:

1. tests/CMakeLists.txt: Add PROPERTIES TIMEOUT 60 to gtest_discover_tests.
   CTest now kills any single test that runs longer than 60 seconds and
   marks it FAILED rather than blocking the entire suite forever.

2. .github/workflows/build-and-test.yml: Add timeout-minutes: 30 to both
   jobs.  If the runner itself becomes unresponsive or a CTest timeout is
   misconfigured, the job is cancelled at 30 minutes instead of occupying
   a runner for GitHub's default 6-hour limit.

3. tests/test_query_timeout.cpp: Rewrite CancelFromAnotherThread and
   TimerFiresOnLongQuery to run SQLExecDirect in a std::packaged_task so
   the main test thread can impose a deadline via wait_for().  If the
   query does not return within the deadline (SQLCancel / driver timer did
   not work), the thread is detached and the test is SKIPPED rather than
   hanging the process.  The unused queryStarted/cancelSent atomics are
   also removed.
The original test held an uncommitted INSERT on conn1 while conn2 did a
SELECT COUNT(*).  On Linux with Firebird embedded mode the engine serializes
page access, so conn2 blocked waiting for conn1's write transaction — causing
a 60-second CTest timeout.

Rewrite to test snapshot isolation without concurrent conflicting transactions:
- conn2 turns off autocommit and captures a snapshot (empty table)
- conn1 inserts and commits (autocommit ON — no concurrent open write)
- conn2 reads within its existing snapshot → must still see 0 rows
  (snapshot isolation: committed changes after snapshot start are invisible)
- conn2 commits, ending the snapshot

This is deterministic and deadlock-free regardless of whether the underlying
Firebird instance uses embedded, SuperServer, or network mode.
x-gha binary provider wrote zero cache entries (confirmed: repo cache listing
shows only old vcpkg-linux-*/vcpkg-windows-* entries, no vcpkg-binary-* from
x-gha). ICU and Firebird were recompiled from source on every single run,
adding 8-20 min per run.

New two-level cache strategy using proven actions/cache@v5:

Level 1 — vcpkg-bincache-{os}-{hash} (binary archives)
  Path: workspace/vcpkg-bincache  (VCPKG_DEFAULT_BINARY_CACHE)
  Per-package .zip archives. restore-keys fallback means only new/changed
  packages are compiled when vcpkg.json changes; everything else restores
  from archive in seconds.

Level 2 — vcpkg-installed-{os}-{hash} (installed tree)
  Path: build/vcpkg_installed
  On exact key hit, vcpkg detects all packages already present and exits in
  under one second, making the entire cmake configure step near-instantaneous.
  No absolute paths in the installed tree (verified), so safe to restore on
  any runner.

Also remove the now-unneeded 'Export GitHub Actions cache environment
variables' (actions/github-script@v7) step from both jobs — it was only
needed by the x-gha provider.

Expected build times after warm cache:
  Linux:   ~2 min total  (was 9 min, was 10-25 min before Phase 15)
  Windows: ~3 min total  (was 20 min)
… deadlock

Two issues found while monitoring run 24008395065:

1. vcpkg caches not saved on test failure (actions/cache default: save-always=false)
   Add save-always: true to all four vcpkg cache steps (bincache + installed,
   windows + linux). Without this, any test failure on the first warm-up run
   would leave the cache empty and force a full recompile on the next run.

2. ConcurrentConnectionTest.ConnectionIsolation still deadlocking at 60 s
   Root cause: Firebird (SuperServer + fbclient) can serialize page access
   between transactions during garbage-collection on user tables, making any
   SELECT on a table where another connection had an active transaction block
   indefinitely — regardless of the isolation level.

   Rewrite the test to use only RDB\ (a single-row system relation):
   - No user DDL/DML  → zero page-lock contention, guaranteed non-blocking
   - Tests what actually matters at the ODBC layer: that setting autocommit on
     one connection handle does not affect another, and that both handles can
     execute queries concurrently and independently
Record the final two-level actions/cache strategy (binary archives + installed
tree with save-always) that replaced the non-functional x-gha provider.
Update version to 4.3 and last-updated date.
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.

1 participant