Skip to content

Implementation descriptors and SQLColAttribute report the columns and parameters they describe (#316) - #317

Open
fdcastel wants to merge 13 commits into
FirebirdSQL:masterfrom
fdcastel:fix/issue-316-ird-record-fill
Open

Implementation descriptors and SQLColAttribute report the columns and parameters they describe (#316)#317
fdcastel wants to merge 13 commits into
FirebirdSQL:masterfrom
fdcastel:fix/issue-316-ird-record-fill

Conversation

@fdcastel

@fdcastel fdcastel commented Sep 7, 2026

Copy link
Copy Markdown
Member

Fixes #316. Builds on #314: its three commits show here until it merges (same function, same test file). This PR is the last ten commits.

A sweep of every path that reports column and parameter metadata (SQLDescribeCol, SQLColAttribute, SQLGetDescField, SQLGetDescRec, SQLCopyDesc, SQLDescribeParam, the IRD and the IPD) over seventeen Firebird column types, before and after binding, in both charsets, found the defects below. All of them are fixed here; the three items under "not changed" are conventions, listed with the reason.

1. Implementation records were empty until the first bind or execute

SQLPrepare sizes the IRD and the IPD but leaves their records at the constructor defaults: type SQL_C_DEFAULT (99) and no name. defFromMetaDataOut / defFromMetaDataIn fill a record only when the column or parameter is bound or the statement executes. SQLDescribeCol, SQLColAttribute and SQLDescribeParam read the statement metadata directly, so they were right, while SQLGetDescField, SQLGetDescRec and SQLCopyDesc on the same descriptors handed back 99, an empty name, nullable 0 and scale 0 with SQL_SUCCESS.

defineImplRecord fills an undefined record of either descriptor before any of the three paths reads or copies it, with the calls SQLBindCol and SQLBindParameter already make.

2. The type fields were the record's raw fields

An implementation record keeps the concise SQL type in type and the converter's C type in conciseType, and the getters handed those out as they are. Measured on the #314 driver after a bind: SQL_DESC_CONCISE_TYPE answered -16 for INTEGER, -25 for BIGINT, -8 for a UTF8 VARCHAR and 1 for a text BLOB while SQLColAttribute answered 4, -5, -9 and -1; SQL_DESC_TYPE answered the concise code (91, 93) for datetime columns instead of SQL_DATETIME; SQL_DESC_DATETIME_INTERVAL_CODE was always 0; SQLGetDescRec returned the same pair. The full matrix is in #316.

sqlGetDescField and sqlGetDescRec derive the verbose type, the concise type and the datetime interval code from the SQL type for both implementation descriptors. The record fields the converter reads are unchanged.

3. SQLColAttribute had the same two gaps

SQL_DESC_TYPE answered 91 for a DATE where the specification asks for SQL_DATETIME, and SQL_DESC_DATETIME_INTERVAL_CODE was rejected as an unknown field. Both now use the same helpers; SQL_DESC_CONCISE_TYPE is unchanged.

4. Three length fields were written as 32-bit integers

SQLGetDescField wrote SQL_DESC_LENGTH, SQL_DESC_OCTET_LENGTH and SQL_DESC_DISPLAY_SIZE as 32-bit integers into the application's SQLULEN / SQLLEN, leaving the upper half untouched on 64-bit builds. They are written at full width now.

5. SQLGetDescRec on an unprepared IRD answered HY091

Now HY007, as SQLGetDescField does since #314.

6. SQLColAttribute and SQLColAttributeW were not exported on Linux

The two entry points declared their numeric attribute as SQLPOINTER on every platform but 64-bit Windows, mirroring Microsoft's header. unixODBC declares it as SQLLEN *, so on Linux the compiler saw an overload rather than the extern "C" function from the header and exported both under C++ names; an ELF symbol scan of the CI build shows 112 of the 114 entry points with C linkage and exactly these two mangled. unixODBC therefore routed every SQLColAttribute call through the ODBC 2 SQLColAttributesW with SQL_DESC_TYPE mapped to SQL_COLUMN_TYPE, and the same #ifdef made the numeric attribute a 32-bit write on Linux x64: SQL_DESC_CONCISE_TYPE of a UTF8 VARCHAR read from a zeroed SQLLEN came back as 4294967287 instead of -9. The parameter is SQLLEN * except on 32-bit Windows now, and the value is written as an SQLLEN everywhere.

Tests

Eight cases in test_descriptor.cpp: IrdRecordsDescribeColumnsAfterPrepare, CopyDescFromIrdAfterPrepare, IrdTypeFieldsFollowOdbc, IpdRecordsDescribeParametersAfterPrepare, IrdLengthFieldsAreFullWidth, IrdGetDescRecBeforePrepareIsHY007, ColAttributeTypeFieldsFollowOdbc, ColAttributeNumericAttributeIsFullWidth. The first seven fail against the #314 driver on Windows, the last one fails on Linux x64 against any earlier build, and pass with this branch. Full suite with this branch, CHARSET=UTF8 and NONE: 399 ran, 234 passed, 165 skipped, 0 failed.

Not changed, and why

  • SQLCopyDesc from an implementation descriptor still copies the converter's C type into the target's concise type. ODBC's SQL and C codes coincide only for some types (SQL_BIGINT and SQL_VARCHAR have no C twin), so an application that copies the IRD into an ARD and fetches through it works today because of that; the copy's SQL_DESC_TYPE is the SQL type.
  • SQL_DESC_LENGTH and SQL_DESC_OCTET_LENGTH for numeric and datetime columns are crossed between the two APIs: SQLColAttribute answers the display size for LENGTH (6 for SMALLINT) and the precision for OCTET_LENGTH (5), the descriptor answers the precision for LENGTH (5) and the display size for OCTET_LENGTH (6), and the specification's transfer octet length would be 2. Unifying them changes values applications size buffers with, so it needs a decision rather than a quiet fix; the descriptor side matches SQLDescribeCol's column size today.
  • CHAR and VARCHAR columns with CHARACTER SET OCTETS are reported as SQL_CHAR / SQL_VARCHAR (a 16-byte one as SQL_GUID, Fix SQL_C_GUID parameter binding sending corrupted data on the wire (#295) #296). Reporting them as SQL_BINARY / SQL_VARBINARY is the Firebird meaning, and an application-visible type change; same reason.

KeysetSizeDoesNotChangeRowArraySize sets SQL_ATTR_KEYSET_SIZE and reads
both it and SQL_ATTR_ROW_ARRAY_SIZE back, then sets SQL_ROWSET_SIZE and
checks the two stay independent.

IrdHeaderFieldsReadableBeforePrepare reads SQL_DESC_ROWS_PROCESSED_PTR,
SQL_DESC_ARRAY_STATUS_PTR and SQL_DESC_ALLOC_TYPE from the IRD of a
statement that has not been prepared. IrdRecordFieldsBeforePrepareAreHY007
expects HY007 for SQL_DESC_COUNT and a record field in that state, and
success for both once the statement is prepared.
sqlSetStmtAttr handled SQL_ATTR_KEYSET_SIZE in the same case as the ODBC 2
SQL_ROWSET_SIZE and stored it in the ARD's array size. An application that
set a keyset size (ODBC 2 applications and generic layers do, with values
such as 100) silently got a rowset of that size, and the next SQLFetch
wrote that many rows into buffers bound for one, with no rows-fetched
counter or status array to warn it.

The value now goes to its own field, read back by sqlGetStmtAttr; the
driver has no keyset-driven cursors, so the number is stored and returned
as the specification allows.
sqlGetDescField refused every field of an undefined descriptor with
HY091 "Invalid descriptor field identifier". For the IRD that state means
"not prepared yet", and the header fields (the array-status and
rows-processed pointers among them, which the application itself set
through SQLSetStmtAttr) do not depend on the result set at all.

The header fields are now returned regardless of the prepared state.
SQL_DESC_COUNT and the record fields of an unprepared IRD answer HY007
"Associated statement is not prepared"; other descriptor types keep the
HY091 they gave before.
…pare

IrdRecordsDescribeColumnsAfterPrepare reads SQL_DESC_TYPE,
SQL_DESC_CONCISE_TYPE and SQL_DESC_NAME with SQLGetDescField, and the
same record with SQLGetDescRec, before anything binds or executes the
columns, and compares with SQLDescribeCol. CopyDescFromIrdAfterPrepare
copies the IRD to an explicit descriptor at the same point and reads the
type back from the copy.
SQLPrepare sizes the IRD but leaves its records at their constructor
defaults (type SQL_C_DEFAULT, no name); defFromMetaDataOut fills a record
only when the column is bound or the statement executes. SQLDescribeCol
and SQLColAttribute read the statement metadata directly, so they were
right while SQLGetDescField, SQLGetDescRec and SQLCopyDesc on the same
IRD handed back 99 and an empty name with SQL_SUCCESS.

The three descriptor paths now fill an undefined IRD record before
reading or copying it, the same call SQLBindCol makes.

Fixes FirebirdSQL#316
IrdTypeFieldsFollowOdbc reads SQL_DESC_TYPE, SQL_DESC_CONCISE_TYPE and
SQL_DESC_DATETIME_INTERVAL_CODE with SQLGetDescField, and type and
subtype with SQLGetDescRec, for INTEGER, VARCHAR, DATE and TIMESTAMP
columns: the concise type is what SQLDescribeCol reports, the verbose
type is SQL_DATETIME with codes 1 and 3 for the two datetime columns.
IrdRecordsDescribeColumnsAfterPrepare now compares the concise type with
SQLDescribeCol as well.
An implementation row record keeps the concise SQL type in `type` and the
converter's C type in `conciseType`, and the descriptor getters handed
those fields out as they are: SQL_DESC_CONCISE_TYPE answered -16 for an
INTEGER column and -8 for a VARCHAR while SQLColAttribute answered 4 and
-9, SQL_DESC_TYPE answered the concise code for datetime columns, and
SQL_DESC_DATETIME_INTERVAL_CODE was always 0.

sqlGetDescField and sqlGetDescRec now derive the verbose type, the
concise type and the datetime interval code from the SQL type for
implementation row records; the record fields the converter reads are
unchanged.
@fdcastel fdcastel changed the title Fill an IRD record from the metadata when the descriptor API reads it (#316) IRD records filled and typed for the descriptor API (#316) Sep 7, 2026
…epare, SQLColAttribute type fields

IpdRecordsDescribeParametersAfterPrepare reads type, concise type,
interval code, scale and nullability of four parameters through
SQLGetDescField and SQLGetDescRec right after SQLPrepare and compares them
with SQLDescribeParam. IrdLengthFieldsAreFullWidth reads SQL_DESC_LENGTH,
SQL_DESC_OCTET_LENGTH and SQL_DESC_DISPLAY_SIZE into a zeroed and into an
all-ones SQLLEN and expects the same value. IrdGetDescRecBeforePrepareIsHY007
expects HY007 from SQLGetDescRec on an unprepared IRD.
ColAttributeTypeFieldsFollowOdbc expects the verbose type, the concise type
and the datetime interval code from SQLColAttribute for INTEGER, VARCHAR,
DATE and TIMESTAMP columns.
…E at full width

sqlGetDescField wrote these three fields as 32-bit integers. Their ODBC
types are SQLULEN and SQLLEN, 64-bit on 64-bit builds, so the upper half
of the application's variable kept whatever it held before the call.
The IPD had the same two defects as the IRD: its records stayed at their
constructor defaults until the parameter was bound, and the type fields
were the record's raw fields. defineImplRecord now fills an undefined
record of either implementation descriptor (input parameters from the
statement's input metadata, procedure output parameters from the output
metadata) before SQLGetDescField, SQLGetDescRec or SQLCopyDesc read it, and
the three type helpers become static members of OdbcDesc so that the
statement code can share them. An application may have stored the verbose
type with an interval code in an IPD record; conciseSqlType folds that
back to the concise code before the other two are derived.

SQLGetDescRec on an unprepared IRD answers HY007, as sqlGetDescField does
since the SQL_ATTR_KEYSET_SIZE change.
…ribute

SQL_DESC_TYPE answered the concise code for datetime columns (91 for a
DATE) where the specification asks for SQL_DATETIME, and
SQL_DESC_DATETIME_INTERVAL_CODE was rejected as an unknown field. Both now
come from the same helpers the descriptor API uses; SQL_DESC_CONCISE_TYPE
is unchanged.
@fdcastel fdcastel changed the title IRD records filled and typed for the descriptor API (#316) Implementation descriptors and SQLColAttribute report the columns and parameters they describe (#316) Sep 7, 2026
ColAttributeNumericAttributeIsFullWidth reads SQL_DESC_CONCISE_TYPE of a
VARCHAR column into a zeroed and into an all-ones SQLLEN and expects
SQLDescribeCol's type from both; a 32-bit write shows a negative code as
a large positive in the first and a corrupted upper half in the second.
The two entry points declared their numeric attribute as SQLPOINTER on
every platform but 64-bit Windows, mirroring Microsoft's header. unixODBC
declares it as SQLLEN *, so on Linux the compiler saw an overload, not the
extern "C" function from the header, and exported both under C++ names
(_Z15SQLColAttributePvttS_sPsS_). unixODBC could not find them and routed
every SQLColAttribute call through the ODBC 2 SQLColAttributesW with the
field identifiers mapped to their ODBC 2 counterparts, so SQL_DESC_TYPE
always came back as the concise type there. The same #ifdef made the
numeric attribute a 32-bit write on Linux x64: a negative type code read
from a zeroed SQLLEN came back as a large positive number.

The parameter is SQLLEN * except on 32-bit Windows, where Microsoft's
header keeps SQLPOINTER, and the value is written as an SQLLEN everywhere.
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.

Implementation descriptors: records empty until the first bind or execute, raw type fields, 32-bit length writes; SQLColAttribute type fields

1 participant