ODBC from C tutorial part 2 — fetching results
Contents
Prerequisites
Before you start this tutorial, ensure you have:
- C You need an ANSI C compiler. For this tutorial we used gcc on Linux, but with a few small alterations the C compilers from Oracle, HP, IBM, and so on will work just as well.
-
ODBC Driver Manager You link your application with an ODBC Driver Manager that:
- Provides the C header files that allow you to compile your application.
- Loads the ODBC driver you want to connect to.
We recommend you use the unixODBC Driver Manager.
You probably already have unixODBC installed if you have the
odbcinst
command. (For Easysoft ODBC drivers, the unixODBC Driver Manager is located in/usr/local/easysoft/unixODBC
, by default, and theodbcinst
command in thebin
subdirectory of that path.)We used unixODBC 2.2.12 in this tutorial. You can find out your unixODBC version with:
odbcinst --version
-
ODBC driver You need an ODBC driver and a working database to connect to.
For this tutorial, we used the Easysoft ODBC-ODBC Bridge as the ODBC driver. We used the ODBC-ODBC Bridge on UNIX to access a remote Microsoft SQL Server database.
Assumptions
This tutorial does not explain the C language and how to write C. We assume you already understand the C programming language, and are able to edit, compile and link programs.
We also assume you have a good ODBC API reference to hand, as this tutorial is not an attempt to reproduce the ODBC Programmer's Reference; it's more example based.
Operating system
This tutorial was developed on UNIX, and we assume you're using UNIX or Linux too. However, all the C examples should work equally well on Microsoft Windows and other operating systems with some minor alterations. (For example, include windows.h
on Microsoft Windows and make the appropriate compiler and linker changes).
ODBC Driver Manager
We assume you're using the unixODBC Driver Manager. All discussion in this document relating to the location and definition of ODBC data sources is for unixODBC.
Fetching results
As described in ODBC from C tutorial (part 1), the process for returning data from a result set consists of:
- Preparing the query.
- Checking the query metadata for the number of columns and the type of those columns.
- Executing the query.
- Fetching each row.
- For each row, fetching each column from that row.
Binding columns
We can combine step 4 and 5 in the above list through a process called binding columns. This involves associating memory with the result set such that at the time SQLFetch
is called, the data for the columns will be copied into the bound memory using the SQLBindCol
call. This call can be treated as an extension to SQLGetData
, but is only called once for each column, before the row is fetched one or more times.
SQLBindCol
arguments are similar to SQLGetData
arguments.
SQLRETURN SQLBindCol( SQLHSTMT StatementHandle, SQLUSMALLINT ColumnNumber, SQLSMALLINT TargetType, SQLPOINTER TargetValue, SQLLEN BufferLength, SQLLEN *StrLen_or_Ind );
The following example uses column-wise binding (the default binding orientation) when calling SQLFetch
.
#include <stdio.h> #include <sql.h> #include <sqlext.h> main() { SQLHENV env; SQLHDBC dbc; SQLHSTMT stmt; SQLSMALLINT columns; /* number of columns in result-set */ SQLCHAR buf[ 5 ][ 64 ]; int row = 0; SQLINTEGER indicator[ 5 ]; int i; /* Allocate an environment handle */ SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); /* We want ODBC 3 support */ SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0); /* Allocate a connection handle */ SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); /* Connect to the DSN mydsn */ /* You will need to change mydsn to one you have created */ /* and tested */ SQLDriverConnect(dbc, NULL, "DSN=mydsn;", SQL_NTS, NULL, 0, NULL, SQL_DRIVER_COMPLETE); /* Allocate a statement handle */ SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); /* Retrieve a list of tables */ SQLTables(stmt, NULL, 0, NULL, 0, NULL, 0, "TABLE", SQL_NTS); /* How many columns are there */ SQLNumResultCols(stmt, &columns); /* Loop through the rows in the result-set binding to */ /* local variables */ for (i = 0; i < columns; i++) { SQLBindCol( stmt, i + 1, SQL_C_CHAR, buf[ i ], sizeof( buf[ i ] ), &indicator[ i ] ); } /* Fetch the data */ while (SQL_SUCCEEDED(SQLFetch(stmt))) { /* display the results that will now be in the bound area's */ for ( i = 0; i < columns; i ++ ) { if (indicator[ i ] == SQL_NULL_DATA) { printf(" Column %u : NULL\n", i); } else { printf(" Column %u : %s\n", i, buf[ i ]); } } } }
A common problem with bound data
Unlike in a SQLGetData
call, the data isn't returned to the area indicated by TargetValue
and StrLen_or_Ind
until the SQLFetch
is made. Because of this, it's important to remember that the values must still be in scope when the SQLFetch
call is made.
The following code fails, or, at the very least, returns unexpected results as the values bound are no longer in scope when SQLFetch
is called:
#include <stdio.h> #include <sql.h> #include <sqlext.h> /* Bind the columns, but by binding to local data, a problem is created */ /* for the future */ int bind_col( SQLHSTMT stmt, int col ) { SQLCHAR buf[ 64 ]; SQLINTEGER indicator; return SQLBindCol( stmt, col + 1, SQL_C_CHAR, buf, sizeof( buf), &indicator ); } /* Fetch the data, the driver will be writing to data out of scope at */ /* this point */ void fetch_data( SQLHSTMT stmt ) { int return; while (SQL_SUCCEEDED(ret = SQLFetch(stmt))) { printf( "fetched\n" ); } } main() { SQLHENV env; SQLHDBC dbc; SQLHSTMT stmt; SQLRETURN ret; /* ODBC API return status */ SQLSMALLINT columns; /* number of columns in result-set */ int row = 0; int i; /* Allocate an environment handle */ SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); /* We want ODBC 3 support */ SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0); /* Allocate a connection handle */ SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); /* Connect to the DSN mydsn */ /* You will need to change mydsn to one you have created and tested */ SQLDriverConnect(dbc, NULL, "DSN=mydsn;", SQL_NTS, NULL, 0, NULL, SQL_DRIVER_COMPLETE); /* Allocate a statement handle */ SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); /* Retrieve a list of tables */ SQLTables(stmt, NULL, 0, NULL, 0, NULL, 0, "TABLE", SQL_NTS); /* How many columns are there */ SQLNumResultCols(stmt, &columns); /* Loop through the rows in the result-set binding to */ /* local variables */ for (i = 0; i < columns; i++) { ret = bind_col( stmt, i ); } fetch_data( stmt ); }
SQLBindCol
and data types
As with SQLGetData
, you specify the type the data is returned in with TargetType
. The values passed into the TargetValue
field must match the type, and there must be sufficient memory available to store the returned information.
Of course, each column may be bound to different types, as this example shows:
#include <stdio.h> #include <sql.h> #include <sqlext.h> main() { SQLHENV env; SQLHDBC dbc; SQLHSTMT stmt; SQLRETURN ret; /* ODBC API return status */ SQLSMALLINT columns; /* number of columns in result-set */ SQLCHAR table[ 64 ]; SQLCHAR column[ 64 ]; SQLINTEGER type; SQLLEN indicator[ 3 ]; int i; /* Allocate an environment handle */ SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); /* We want ODBC 3 support */ SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0); /* Allocate a connection handle */ SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); /* Connect to the DSN mydsn */ /* You will need to change mydsn to one you have created and tested */ SQLDriverConnect(dbc, NULL, "DSN=mydsn;", SQL_NTS, NULL, 0, NULL, SQL_DRIVER_COMPLETE); /* Allocate a statement handle */ SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); /* Retrieve a list of columns */ SQLColumns(stmt, NULL, 0, NULL, 0, "tablename", SQL_NTS, NULL, 0); ret = SQLBindCol( stmt, 3, SQL_C_CHAR, table, sizeof( table ), &indicator[ 0 ] ); ret = SQLBindCol( stmt, 4, SQL_C_CHAR, column, sizeof( column ), &indicator[ 1 ] ); ret = SQLBindCol( stmt, 5, SQL_C_LONG, &type, 0, &indicator[ 2 ] ); /* Fetch the data */ while (SQL_SUCCEEDED(ret = SQLFetch(stmt))) { printf( "%s.%s type %d\n", table, column, type ); } }
SQLBindCol
and SQLGetData
The previous example also shows another feature of SQLBindCol
— not all the columns in a result set need to be bound.
It's possible to combine binding columns and fetching data with SQLGetData
, but many ODBC drivers have restrictions on how this can be done. One common restriction is that SQLGetData
may only be called on columns after the last bound column. The application can query the ODBC driver to find out how calls to SQLGetData
and bound parameters may be intermixed.
The SQLGetInfo
value, SQL_GETDATA_EXTENSIONS
, returns a bitmask with the following values that relate to bound columns.
Value | Description |
---|---|
SQL_GD_ANY_COLUMN |
SQLGetData can be called for any unbound column, including those before the last bound column. Note that the columns must be called in order of ascending column number unless SQL_GD_ANY_ORDER is also returned. |
SQL_GD_ANY_ORDER |
SQLGetData can be called for unbound columns in any order. Note that SQLGetData can only be called for columns after the last bound column unless SQL_GD_ANY_COLUMN is also returned. |
SQL_GB_BOUND |
SQLGetData can be called for bound columns as well as unbound columns. An ODBC driver cannot return this value unless it also returns SQL_GD_ANY_COLUMN . |
SQLGetData
is only required to return data from unbound columns that occur after the last bound column.
Returning multiple rows
The previous section described how to bind columns and return data one row at a time. The next step is to return data for more than one row at once. SQLBindCol
allows this to be done, by binding the address of an array of values to transfer into instead of a single value.
Preparing to return multiple rows
To return multiple rows, you can use either SQLExtendedFetch
or its ODBC 3 equivalent SQLFetchScroll
. To specify the size of the rowset, the Statement
attribute SQL_ROWSET_SIZE
(SQLExtendedFetch
) or SQL_ATTR_ROW_ARRAY_SIZE
(SQLFetchScroll
) must be set to the required value by using a SQLSetStmtAttr
call. Then, memory bound to the columns using SQLBindCol
must be large enough to contain the returned data. For example, if the type was bound as a SQL_C_LONG
, that is a 4 byte value; if the row size was 20, the bound memory must have a length of 80 bytes. The length of each array element must be the same size, so if binding to a VARCHAR(30)
field, the application would bind 31 characters (including the space for the null
) for each row, and pass 31 into the buffer length field in the SQLBindCol
call.
For example:
/* variables to contain returned data */ SQLCHAR table[ 20 ][ 31 ]; SQLCHAR column[ 20 ][ 31 ]; SQLINTEGER type[ 20 ]; SQLLEN indicator1[ 20 ], indicator2[ 20 ], indicator3[ 20 ]; /* Set the row size to 20 */ /* If you are using SQLExtendedFetch rather than SQLFetchScroll to fetch the data, */ /* replace SQL_ATTR_ROW_ARRAY_SIZE with SQL_ROWSET_SIZE */ SQLSetStmtAttr( stmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER) 20, 0 ); /* Retrieve a list of columns */ SQLColumns(stmt, NULL, 0, NULL, 0, "tablename", SQL_NTS, NULL, 0); ret = SQLBindCol( stmt, 3, SQL_C_CHAR, table, sizeof( table[ 0 ] ), indicator1 ); ret = SQLBindCol( stmt, 4, SQL_C_CHAR, column, sizeof( column[ 0 ] ), indicator2 ); ret = SQLBindCol( stmt, 5, SQL_C_LONG, &type, 0, indicator3 );
Notice that the indicator variables also become an array of SQLLEN
values. After the fetch, each element will contain the length of the data, or SQL_NULL_DATA
if the column is null
.
The SQLFetch
call returns the data into the arrays.
SQLExtendedFetch
The SQLExtendedFetch
call allows the application to pass in arguments that indicate how many rows were read from the database. While the application may request 10 rows, the database may only have 5 remaining to return. The arguments used in SQLExtendedFetch
are as follows:
SQLRETURN SQLExtendedFetch( SQLHSTMT StatementHandle, SQLUSMALLINT FetchOrientation, SQLINTEGER FetchOffset, SQLUINTEGER *RowCountPtr, SQLUSMALLINT *RowStatusArray);
where the above arguments are:
Argument | Description |
---|---|
StatementHandle |
The statement handle. |
FetchOrientation |
The direction of the fetch. In this case, we use SQL_FETCH_NEXT to return the next n rows, where n is the value set in SQL_ROWSET_SIZE . |
FetchOffset |
The row number to return, in the case of a SQL_FETCH_NEXT , this is ignored. |
RowCountPtr |
The address of a value that will contain the number of rows returned after the call. This will be between 0 and SQL_ROWSET_SIZE. |
RowStatusArray |
An array of status values that will be returned, one for each row returned. |
Each element of the RowStatusArray
array contains one of the following values:
Value | Description |
---|---|
SQL_ROW_SUCCESS |
The row was successfully returned. |
SQL_ROW_SUCCESS_WITH_INFO |
The row was returned but there were one or more warnings generated. This could, for example, indicate that there was insufficient space allocated to contain the data. Return the warning information as normal. |
SQL_ROW_ERROR |
The row was not returned and one or more errors occurred. Return the warning information as normal. |
SQL_ROW_NOROW |
No row was returned, normally indicating that the end of the result set has been reached. |
The use of this call is shown in the following code, (which could follow on from the previous snippet):
SQLROWSETSIZE row_count; SQLUSMALLINT row_status[ 20 ]; SQLRETURN ret; do { ret = SQLExtendedFetch( stmt, SQL_FETCH_NEXT, 0, &row_count, row_status ); if ( SQL_SUCCEEDED( ret )) { int row; /* display each row */ for ( row = 0; row < row_count; row ++ ) { printf( "Row %d >", row ); if ( row_status[ row ] == SQL_ERROR ) { printf( "ROW ERROR\n" ); } else if ( row_status[ row ] == SQL_SUCCESS || row_status[ row ] == SQL_SUCCESS_WITH_INFO ) { /* display data */ if ( indicator1[ row ] == SQL_NULL_DATA ) { printf( "NULL<>" ); } else { printf( "%s<>", table[ row ] ); } if ( indicator2[ row ] == SQL_NULL_DATA ) { printf( "NULL<>" ); } else { printf( "%s<>", column[ row ] ); } if ( indicator2[ row ] == SQL_NULL_DATA ) { printf( "NULL<" ); } else { printf( "%d<", type[ row ] ); } printf( "\n" ); } } } } while( SQL_SUCCEEDED( ret ) && row_count == 20 );
The above code demonstrates an important point: the SQLExtendedFetch
call returns SQL_SUCCESS
when only part of a SQL_ROWSET_SIZE
set of rows is returned. This can be checked by using the pcrow
value. If there a no rows to return, the call returns SQL_NO_DATA
in the same way as the single row case (described in the basic fetching results section).
SQLFetchScroll
The ODBC 3 version of SQLExtendedFetch
is SQLFetchScroll
. The arguments to this are:
SQLRETURN SQLFetchScroll( SQLHSTMT StatementHandle, SQLSMALLINT FetchOrientation, SQLROWOFFSET FetchOffset);
There is no reference to the row_count
or row_status
arguments of the SQLExtendedFetch
call. These values in ODBC 3 are now replaced by the statement attributes SQL_ATTR_ROW_STATUS_PTR
and SQL_ATTR_ROWS_FETCHED_PTR
. The use of these statement attributes is shown in the following code, which is functionally equivalent to the previous example, but uses SQLFetchScroll
instead of SQLExtendedFetch
..
SQLROWSETSIZE row_count; SQLUSMALLINT row_status[ 20 ]; SQLRETURN ret; /* * set up the values to return fetch information into */ SQLSetStmtAttr( stmt, SQL_ATTR_ROWS_FETCHED_PTR, &row_count, 0 ); SQLSetStmtAttr( stmt, SQL_ATTR_ROW_STATUS_PTR, row_status, 0 ); do { ret = SQLFetchScroll( stmt, SQL_FETCH_NEXT, 0 ); if ( SQL_SUCCEEDED( ret )) { int row; /* display each row */ for ( row = 0; row < row_count; row ++ ) { printf( "Row %d >", row ); if ( row_status[ row ] == SQL_ERROR ) { printf( "ROW ERROR\n" ); } else if ( row_status[ row ] == SQL_SUCCESS || row_status[ row ] == SQL_SUCCESS_WITH_INFO ) { /* display data */ if ( indicator1[ row ] == SQL_NULL_DATA ) { printf( "NULL<>" ); } else { printf( "%s<>", table[ row ] ); } if ( indicator2[ row ] == SQL_NULL_DATA ) { printf( "NULL<>" ); } else { printf( "%s<>", column[ row ] ); } if ( indicator2[ row ] == SQL_NULL_DATA ) { printf( "NULL<" ); } else { printf( "%d<", type[ row ] ); } printf( "\n" ); } } } } while( SQL_SUCCEEDED( ret ) && row_count == 20 );