ODBC from C Tutorial Part 1

Contents

Introduction

This is part 1 of a series of Easysoft tutorials on using ODBC from C.

Pre-requisites

Before you start part 1 of this tutorial you need to ensure you satisfy all the pre-requisites:

  1. C

    You will need an ANSI C compiler. For this tutorial we used gcc on Linux, but with a few small alterations the C compilers from Sun, HP, IBM etc will work just as well.

  2. ODBC Driver Manager

    You link your application with an ODBC Driver Manager which will:

    • provide the C header files that allow you to compile your application.
    • load the driver you want to connect to.

    We recommend you use the unixODBC driver manager.

    You probably have the unixODBC driver manager installed if you have the odbcinst command (for ODBC drivers from Easysoft, the unixODBC driver manager is located in /usr/local/easysoft/unixODBC and the odbcinst command in the bin sub directory of that path).

    We used unixODBC 2.2.12 in this tutorial. You can find out the unixODBC version you are using with:

      odbcinst --version
      
  3. ODBC Driver

    You will need an ODBC driver and a working database to connect to. Easysoft can supply ODBC drivers for many databases and operating systems. All Easysoft ODBC drivers include the unixODBC driver manager.

    For this tutorial we used the Easysoft ODBC-ODBC Bridge as the ODBC driver to access a remote MS SQL Server database from UNIX.

Assumptions

This tutorial does not explain the C language and how to write C. It is expected you already understand the C programming language, and are able to edit, compile and link programs.

We have assumed you have a good ODBC API reference to hand (see ODBC Reference) as this tutorial is not an attempt to just reproduce the ODBC Programmers Reference; it is more example based.

Operating System

This tutorial was designed on UNIX and we have assumed you are using UNIX too. However, all the C examples should work equally well on MS Windows and other operating systems with some minor alterations (e.g. including windows.h on MS Windows and making the appropriate compiler/linker changes).

ODBC Driver Manager

We have assumed you are using the unixODBC driver manager. All discussion in this document relating to the location and definition of ODBC data sources is for unixODBC.

The ODBC API

ODBC Reference

The ODBC Application Programming Interface (API) defines the functions your application can call in the ODBC driver manager and the arguments they take.

The reference for the ODBC API is The Microsoft ODBC 3.0 Programmers Reference Volume 1 and 2 (ISBN 1-57231-516-4) although you may have some trouble finding this now. You can also find the entire ODBC reference in older versions of the ODBC SDK and online at Microsoft's web site.

ODBC Include Files

For C programmers the definitions you require are held in the header files sql.h, sqlext.h, sqltypes.h, sqlucode.h and odbcinst.h. For unixODBC you will find these installed in <installpath>/include. Further explanation of these headers can be found later.

Normally you only need to include sql.h (which contains most of the definitions you'll need) and sqlext.h (which contains mostly additions for ODBC 3). sqlucode.h is automatically included by sqlext.h and sqltypes.h is automatically included from sql.h. odbcinst.h contains the installers APIs e.g. SQLConfigDataSource and the APIs a drivers use e.g. SQLWriteFileDSN.

ODBC Defined Macros

If you examine the ODBC header files you'll find tests on a number of macros. The principal macros examined are:

There are also a number of convenience macros like SQL_SUCCEEDED (used to test ODBC API return status, see Handling and reporting ODBC errors and diagnostics) and macros used to describe something, like SQL_NTS (which specifies a provided string is null terminated).

ODBC Handles

In ODBC there are four main handle types and you will need to know at least three to do anything useful:

ODBC handles are opaque types and although the ODBC standard does not specifically say they are pointers to structures they generally are (but you should not rely on this).

String Arguments

A number of the ODBC APIs accept string pointers and return strings to user supplied buffers. In general, ODBC APIs accepting input string arguments have a pointer argument followed by a length argument e.g.

SQLRETURN SQLPrepare(SQLHSTMT stmt,
                     SQLCHAR *StatementText,
                     SQLINTEGER TextLength)

There are usually two ways of specifying the length of an input string:

  1. The length in bytes of the string e.g. "fred" has length 4.
  2. The string is null terminated (as is usual in C). You specify null terminated strings with the ODBC defined macro SQL_NTS.

Where an ODBC API returns a string it is usual for the API to require a pointer to a buffer and a pointer to an integer (of some sort) to return the length of the returned string e.g.

SQLRETURN SQLGetCursorName(SQLHSTMT StatementHandle,
                           SQLCHAR *CursorName,
                           SQLSMALLINT BufferLength,
                           SQLSMALLINT *NameLengthPtr)

In the above case you pass a pointer to a buffer to receive the cursor name, the length of that buffer (so it is not overrun) and a pointer to a SQLSMALLINT in which is written the length of the returned string. There are a few useful points about the way the ODBC API works here which is worth mentioning:

  1. In general, the buffer must be big enough for the returned string and a terminating null character. The returned length will not include the terminating null character.
  2. APIs like the above generally return SQL_SUCCESS_WITH_INFO, a state of 01004 and a message of "String data, right truncated" if the supplied buffer was not big enough for the returned string.
  3. You do not have to supply a pointer to the returned length (i.e. it may be NULL) but then if the buffer is truncated you will not know how long big a buffer you need.
  4. Strangely, you do not have to specify a buffer (i.e. it may be NULL) and the buffer length may be zero. It is not uncommon to see applications do something like this:
    SQLCHAR *buffer = NULL;
    SQLSMALLINT retlen;
    SQLRETURN ret;
    
    ret = SQLGetCursorName(stmt, NULL, 0, &retlen);
    if (SQL_SUCCEEDED(ret)) {
        buffer = malloc(retlen + 1); /* add one for null termination */
        SQLGetCursorName(stmt, buffer, retlen + 1, NULL);
    }
    
    

Allocating and Freeing Handles

As described in ODBC Handles there are four types of ODBC handle; environment, connection, statement and descriptor. Handles must be allocated in a specific order and each type of handle is used for different purposes. Each handle type has attributes which you can query and set and a diagnostic stack which can be queried for errors and diagnostic information.

Each handle type can be allocating using SQLAllocHandle:

SQLRETURN SQLAllocHandle(
  SQLSMALLINT HandleType,
  SQLHANDLE InputHandle,
  SQLHANDLE *OutputHandlePtr)

HandleType must be one of:

The InputHandle argument is either SQL_NULL_HANDLE (when allocating environment handles) or the value of the enclosing handle i.e. you pass an environment handle when allocating a connection handle and a connection handle when allocating a statement or descriptor handle.

OutputHandlePtr is a ptr to the handle to be returned.

Similarly, there is the SQLFreeHandle API to free up a handle and its associated resources:

SQLRETURN SQLFreeHandle(
  SQLSMALLINT HandleType,
  SQLHANDLE Handle)

Note that handles generally need to be freed in the opposite order to which they were allocated and that handles cannot be freed if they are in use e.g. you cannot free a connected connection handle until it is disconnected (see later).

Ignoring descriptors for now, the following code is usually in every ODBC application:

SQLHENV env;
SQLHDBC dbc;
SQLHSTMT stmt;

SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
/* connect to the data source */
SQLAllocHandle(SQL_HANDLE_ENV, dbc, &stmt);
/* do something with the statement handle e.g. issue sql */
SQLFreeHandle(SQL_HANDLE_STMT, stmt);
/* disconnect */
SQLFreeHandle(SQL_HANDLE_DBC, dbc);
SQLFreeHandle(SQL_HANDLE_ENV, env);

In ODBC 2.0 descriptor handles did not exist and each handle type had its own allocation and deallocation API; SQLAllocEnv, SQLFreeEnv, SQLAllocConnect, SQLFreeConnect, SQLAllocStmt and SQLFreeStmt. You should avoid using these APIs now and use SQLAllocHandle and SQLFreeHandle instead.

Setting Your Environment

You set environment attributes with SQLSetEnvAttr and retrieve them with SQLGetEnvAttr.

Once you've allocated your environment handle you specify the ODBC version behavior you require (in ODBC 3.0 this is compulsory and if you forget to do this and attempt to use an environment handle before specifying the ODBC behavior your require you will get a HY010, function sequence error). Currently there are only two choices; SQL_OV_ODBC2 (for ODBC 2 behavior) and SQL_OV_ODBC3 (for ODBC 3 behavior).

If you ask for ODBC 3 behavior:

New applications should always ask for ODBC 3 behavior:

SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0);

A few ODBC drivers still do not support ODBC 3 properly but this does not really matter as the driver manager will translate for your application. If you ask for ODBC 3 behavior and the driver does not support ODBC 3, the driver manager will return SQL_SUCCESS_WITH_INFO from SQLDriverConnect and a state of HYC00 (The driver does not support the version of ODBC behavior that the application requested); this is not an error.

There are other environment attributes you may set like SQL_ATTR_CONNECTION_POOLING, SQL_ATTR_CP_MATCH and SQL_ATTR_OUTPUT_NTS but they are beyond the scope of this introductory tutorial (see ODBC Reference).

Listing Installed Drivers and Data Sources

Listing Installed Drivers

When you are using unixODBC you can list installed ODBC drivers using:

odbcinst -q -d

and you can view them in ODBCConfig just as in Windows in the ODBC Administrator.

To programmatically query for drivers you use SQLDrivers like this:

#include <stdio.h>
#include <sql.h>
#include <sqlext.h>

main() {
  SQLHENV env;
  char driver[256];
  char attr[256];
  SQLSMALLINT driver_ret;
  SQLSMALLINT attr_ret;
  SQLUSMALLINT direction;
  SQLRETURN ret;

  SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
  SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0);

  direction = SQL_FETCH_FIRST;
  while(SQL_SUCCEEDED(ret = SQLDrivers(env, direction,
				       driver, sizeof(driver), &driver_ret,
				       attr, sizeof(attr), &attr_ret))) {
    direction = SQL_FETCH_NEXT;
    printf("%s - %s\n", driver, attr);
    if (ret == SQL_SUCCESS_WITH_INFO) printf("\tdata truncation\n");
  }
}

which when run produces output like this:

OOB - Easysoft ODBC-ODBC Bridge
PostgreSQL - Postgres SQL Driver
EASYSOFT_ISAM - Easysoft Data Access for ISAM

You can use the returned driver name (the driver variable in the above example) when calling SQLDriverConnect to use DSN-less connections.

Listing Installed Data Sources

When you are using unixODBC you can list installed data sources using:

odbcinst -q -s

and you can view them in ODBCConfig just as in Windows in the ODBC Administrator.

To programmatically query for data sources you use SQLDataSources like this:

#include <stdio.h>
#include <sql.h>
#include <sqlext.h>

main() {
  SQLHENV env;
  char dsn[256];
  char desc[256];
  SQLSMALLINT dsn_ret;
  SQLSMALLINT desc_ret;
  SQLUSMALLINT direction;
  SQLRETURN ret;

  SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
  SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0);

  direction = SQL_FETCH_FIRST;
  while(SQL_SUCCEEDED(ret = SQLDataSources(env, direction,
					   dsn, sizeof(dsn), &dsn_ret,
					   desc, sizeof(desc), &desc_ret))) {
    direction = SQL_FETCH_NEXT;
    printf("%s - %s\n", dsn, desc);
    if (ret == SQL_SUCCESS_WITH_INFO) printf("\tdata truncation\n");
  }
}

which when run produces output like this:

mydsn - OOB
pdsn - Postgres
sample_isam - EASYSOFT_ISAM

You can use the returned DSN name (the dsn variable in the above example) when calling SQLDriverConnect to connect to that DSN.

Connecting to a Driver or Data Source

Once you have a connection handle you can connect to your ODBC driver or data source using SQLDriverConnect (ODBC 2 applications use SQLConnect but this is much less flexible). Connecting to your ODBC driver is perhaps one of the largest subjects in ODBC as SQLDriverConnect can be called in many different ways; this introductory tutorial will cover the simplest case of connecting to a named data source you have already created and further tutorials will expand on this.

SQLDriverConnect

The SQLDriverConnect API is:

SQLRETURN SQLDriverConnect(
  SQLHDBC ConnectionHandle,
  SQLHWND WindowHandle,
  SQLCHAR *InConnectionString,
  SQLSMALLINT StringLength1,
  SQLCHAR *OutConnectionString,
  SQLSMALLINT BufferLength,
  SQLSMALLINT *StringLength2Ptr,
  SQLUSMALLINT DriverCompletion)

Rather than explain every argument in detail (there are plenty of references which do this) we'll provide the two simplest ways of using SQLDriverConnect. The most important arguments are ConnectionHandle, InConnectionString, StringLength1 and DriverCompletion as these are the minimum required to connect to a data source.

A Simple Connection

First you must create your named data source (DSN). How you do this depends on the platform and the driver. In Windows and unix GUI (Graphical User Interface) environments you start the ODBC Administrator and select your driver where you will be presented with a dialog you use to name and define the data source. Practically, in unix, very few drivers come with the ability to create a DSN via a GUI interface and in this case you can define your data source in a odbc.ini file using an editor (the other tutorials on this site will help you with this process).

so if you have created a DSN called "mydsn" then the simplest call is:

SQLRETURN ret;
SQLHDBC dbc; /* assume already allocated */

ret = SQLDriverConnect(dbc, NULL, "DSN=mydsn;", SQL_NTS,
                       NULL, 0, NULL, SQL_DRIVER_COMPLETE);

In this call we have provided the DSN name and asked the driver to complete the connection string but also we do not want an output connection string or any dialogs (WindowHandle is NULL). The driver manager will look at the "mydsn" DSN to find out what driver is required, load the driver and call SQLDriverConnect in the driver with the same arguments. The driver will look up the mydsn DSN with SQLGetPrivateProfileString and retrieve all the attributes defined for that data source. So long as the driver now has enough information to connect to the data source it will.

Format of the ODBC Connection String

The ODBC connection string contains a series of attributes names and values separated by semi-colons:

connection-string::= empty-string[;] | attribute[;] | attribute; connection-string
empty-string ::=
attribute ::= attribute-keyword=attribute-value | DRIVER=[{]attribute-value[}]
attribute-keyword ::= DSN | UID | PWD | driver-defined-attribute-keyword
attribute-value ::= character-string
driver-defined-attribute-keyword = identifier

where character-string has zero or more characters; identifier has one or more characters; attribute-keyword is not case-sensitive; attribute-value may be case-sensitive; and the value of the DSN keyword does not consist solely of blanks. Due to the connection string grammar, keywords and attribute values that contain the characters []{}(),;?*=!@ should be avoided. The value of the DSN keyword cannot consist only of blanks, and should not contain leading blanks. Because of the grammar of the system information, keywords and data source names cannot contain the backslash (\) character. Applications do not have to add braces around the attribute value after the DRIVER keyword unless the attribute contains a semicolon (;), in which case the braces are required. If the attribute value that the driver receives includes the braces, the driver should not remove them, but they should be part of the returned connection string.

ODBC Defined Connection Attributes

The key to the previous example is the InConnectionString argument which can contain some ODBC defined attributes (there are also driver defined attributes but these vary per driver). The ODBC defined attributes are:

Returned Connection String

The connection string returned by SQLDriverConnect can be used to reconnect at a later date. You may ask why bother returning a connection string when one was passed in which presumably was sufficient to connect in the first place. The reasons for this are:

  1. If you use SQL_DRIVER_COMPLETE the driver is free to retrieve any other attributes it needs to connect in addition to those you passed in.
  2. If a dialog was thrown the user may have entered additional or changed values.

A couple of small examples with the Easysoft ODBC-ODBC Bridge (OOB) illustrate this:

  1. Assume you have created a DSN called fred with all the necessary attributes entered and you call SQLDriverConnect with the connection string "DSN=fred;" and SQL_DRIVER_COMPLETE. OOB is free to examine the DSN fred and retrieve the attributes it requires such as ServerPort, TargetDSN, LogonUser and LogonAuth. The returned connection string will look like "DSN=fred;ServerPort=myserver:8888;TargetDSN=mydsn;LogonUser=me;LogonAuth=password;". Now if the application stores the returned string it can reconnect without reference to the DSN so the DSN can be changed without affecting the connection made using the stored string.
  2. Assume you have created a DSN called fred but not filled all the fields in; perhaps because you wanted OOB to throw a dialog requesting the field you omitted. You call SQLDriverConnect with "DSN=fred;" and SQL_DRIVER_COMPLETE. OOB will throw its dialog allowing you to enter the missing values and then return a full connection string which can be used to reconnect later without the need for user interaction.

Full Connection Example

In A simple connection we only used the minimum number of arguments to SQLDriverConnect to get connected. This is an example utilising all arguments.

#include <stdio.h>
#include <sql.h>
#include <sqlext.h>

/*
 * see Retrieving ODBC Diagnostics
 * for a definition of extract_error().
 */
static void extract_error(
    char *fn,
    SQLHANDLE handle,
    SQLSMALLINT type);

main() {
  SQLHENV env;
  SQLHDBC dbc;
  SQLHSTMT stmt;
  SQLRETURN ret; /* ODBC API return status */
  SQLCHAR outstr[1024];
  SQLSMALLINT outstrlen;

  /* 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 */
  ret = SQLDriverConnect(dbc, NULL, "DSN=fred;", SQL_NTS,
			 outstr, sizeof(outstr), &outstrlen,
			 SQL_DRIVER_COMPLETE);
  if (SQL_SUCCEEDED(ret)) {
    printf("Connected\n");
    printf("Returned connection string was:\n\t%s\n", outstr);
    if (ret == SQL_SUCCESS_WITH_INFO) {
      printf("Driver reported the following diagnostics\n");
      extract_error("SQLDriverConnect", dbc, SQL_HANDLE_DBC);
    }
    SQLDisconnect(dbc);		/* disconnect from driver */
  } else {
    fprintf(stderr, "Failed to connect\n");
    extract_error("SQLDriverConnect", dbc, SQL_HANDLE_DBC);
  }
  /* free up allocated handles */
  SQLFreeHandle(SQL_HANDLE_DBC, dbc);
  SQLFreeHandle(SQL_HANDLE_ENV, env);
}

Sample output from running this code when "fred" is an OOB data source connecting to MS SQL Server:

sh-2.05$ ./a.out 
Connected
Returned connection string was:
        DSN=fred;SERVERPORT=server:8888;TARGETDSN=test;UID=dbuser;PWD=dbpass;
                LOGONUSER=fred;LOGONAUTH=mypass;
Driver reported the following diagnostics

The driver reported the following diagnostics whilst running SQLDriverConnect

01000:1:5703:[unixODBC][NetConn: 141dbe18][Microsoft][ODBC SQL Server Driver][SQL Server]
        Changed language setting to us_english.
01000:2:5701:[unixODBC][NetConn: 141dbe18][Microsoft][ODBC SQL Server Driver][SQL Server]
        Changed database context to 'easysoft_fred'.

Driver and Driver Manager Information

When you start working with ODBC you will inevitably find differences between ODBC drivers. ODBC defines a set of informational types you can retrieve which describe the ODBC driver, driver manager and data source. You use SQLGetInfo to retrieve this information.

SQLRETURN SQLGetInfo(SQLHDBC ConnectionHandle,
                     SQLUSMALLINT InfoType,
		     SQLPOINTER InfoValuePtr,
		     SQLSMALLINT BufferLength,
		     SQLSMALLINT StringLengthPtr)

There are basically 3 types of information returned - strings, bitmasks and integer values. String information types are sometimes binary values represented by 'Y' and 'N'. To retrieve an informational type, look it up in the ODBC specification to find out what type it is and then use one of the following:

SQLCHAR string_val[1024];
SQLSMALLINT string_len;
SQLRETURN ret;
SQLUINTEGER u_val;
SQLUSMALLINT su_val;

/* for string values */
ret = SQLGetInfo(dbc,
                 SQL_XXX, /* e.g. SQL_DATA_SOURCE_NAME */
		 string_val,
		 sizeof(string_val),
		 &string_len);
if (SQL_SUCCEEDED(ret)) {
   if (ret == SQL_SUCCESS_WITH_INFO)
       printf("buffer too small, string truncated\n");
   printf("Returned value is %s\n", string_val);
} else {
  /* error */
}

/* for SQLUINTEGER values - mostly bitmasks */
ret = SQLGetInfo(dbc,
                 SQL_XXX, /* e.g. SQL_INSERT_STATEMENT */
		 (SQLPOINTER)&u_val,
		 0, /* ignored for SQLUINTEGER types */
		 0); /* ignored for SQLUINTEGER types */
if (SQL_SUCCEEDED(ret)) {
  printf("Returned value is %lx\n", u_val);
} else {
  /* error */
}

/* for SQLUSMALLINT values - mostly counts/limits */
ret = SQLGetInfo(dbc,
                 SQL_XXX, /* e.g. SQL_MAX_CONCURRENT_ACTIVITIES */
		 (SQLPOINTER)&su_val,
		 0, /* ignored for SQLUSMALLINT types */
		 0); /* ignored for SQLUSMALLINT types */
if (SQL_SUCCEEDED(ret)) {
  printf("Returned value is %u\n", su_val);
} else {
  /* error */
}

Note Most InfoTypes require the connection handle to be in a connected state since they are returning information about the driver.

A small example of driver information (without error checking):

#include <stdio.h>
#include <sql.h>
#include <sqlext.h>

main() {
  SQLHENV env;
  SQLHDBC dbc;
  SQLRETURN ret; /* ODBC API return status */

  /* 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 */
  ret = SQLDriverConnect(dbc, NULL, "DSN=web;", SQL_NTS,
			 NULL, 0, NULL,
			 SQL_DRIVER_COMPLETE);
  if (SQL_SUCCEEDED(ret)) {
    SQLCHAR dbms_name[256], dbms_ver[256];
    SQLUINTEGER getdata_support;
    SQLUSMALLINT max_concur_act;
    SQLSMALLINT string_len;

    printf("Connected\n");
    /*
     *  Find something out about the driver.
     */
    SQLGetInfo(dbc, SQL_DBMS_NAME, (SQLPOINTER)dbms_name,
	       sizeof(dbms_name), NULL);
    SQLGetInfo(dbc, SQL_DBMS_VER, (SQLPOINTER)dbms_ver,
	       sizeof(dbms_ver), NULL);
    SQLGetInfo(dbc, SQL_GETDATA_EXTENSIONS, (SQLPOINTER)&getdata_support,
	       0, 0);
    SQLGetInfo(dbc, SQL_MAX_CONCURRENT_ACTIVITIES, &max_concur_act, 0, 0);

    printf("DBMS Name: %s\n", dbms_name);
    printf("DBMS Version: %s\n", dbms_ver);
    if (max_concur_act == 0) {
      printf("SQL_MAX_CONCURRENT_ACTIVITIES - no limit or undefined\n");
    } else {
      printf("SQL_MAX_CONCURRENT_ACTIVITIES = %u\n", max_concur_act);
    }
    if (getdata_support & SQL_GD_ANY_ORDER)
      printf("SQLGetData - columns can be retrieved in any order\n");
    else
      printf("SQLGetData - columns must be retrieved in order\n");
    if (getdata_support & SQL_GD_ANY_COLUMN)
      printf("SQLGetData - can retrieve columns before last bound one\n");
    else
      printf("SQLGetData - columns must be retrieved after last bound one\n");

    SQLDisconnect(dbc);		/* disconnect from driver */
  } else {
    fprintf(stderr, "Failed to connect\n");

  }
  /* free up allocated handles */
  SQLFreeHandle(SQL_HANDLE_DBC, dbc);
  SQLFreeHandle(SQL_HANDLE_ENV, env);
}

which to MS SQL Server via OOB produces output like this:

Connected
DBMS Name: Microsoft SQL Server
DBMS Version: 08.00.0760
SQL_MAX_CONCURRENT_ACTIVITIES = 1
SQLGetData - columns must be retrieved in order
SQLGetData - columns must be retrieved after last bound one

Basic Result-set Generating Functions

Result-set generating APIs are the work force of ODBC since they allow you to obtain metadata and issue SQL querying your database. A result-set is a list of rows and columns in a statement which you can retrieve e.g. when you call SQLTables to get a list of tables you will get a result-set with 0 or more rows (one per table or view) and each row containing columns containing information about the table.

For the meta-data APIs which generate result-sets the ODBC specification states what the result-set looks like e.g. for SQLTables you get rows of 5 columns called TABLE_CAT, TABLE_SCHEM, TABLE_NAME, TABLE_TYPE and REMARKS.

The basic meta-data APIs which generate result-sets we'll mention here are:

Other meta-data APIs returning result-sets are SQLTablePrivileges, SQLStatistics, SQLSpecialColumns, SQLProcedures, SQLProcedureColumns, SQLPrimaryKeys, SQLForeignKeys, SQLColumnPrivileges and SQLGetTypeInfo.

To issue queries on your database you use SQLPrepare followed by SQLExecute or you call SQLExecDirect. Here are some examples:

Basic Fetching Results

Now we've covered creating handles, connection and basic result-set generating functions we can actually write a complete program which does something useful i.e. retrieves some useful information from the database.

The general form for fetching results is:

  1. call some result-set generating ODBC API
  2. call SQLNumResultCols to find out how many columns are in the result-set.
  3. Optionally call SQLDescribeCol or SQLColAttribute to find out meta data for the columns.
  4. call SQLFetch to retrieve a row. If SQLFetch returns SQL_NO_DATA there are no rows in the result-set.
  5. loop through columns calling SQLGetData to retrieve the column data.
  6. go back to 4 until SQLFetch returns SQL_NO_DATA.

This small example illustrates the most basic operations; connect to the database, issue a request and retrieve the results. This example does not attempt to cover all the ODBC APIs used completely and does not include proper error checking but it is a working program which we can also use to cover compilation and linking.

#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 */
int row = 0;

/* 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 */
while (SQL_SUCCEEDED(ret = SQLFetch(stmt))) {
    SQLUSMALLINT i;
    printf("Row %d\n", row++);
    /* Loop through the columns */
    for (i = 1; i <= columns; i++) {
        SQLINTEGER indicator;
        char buf[512];
        /* retrieve column data as a string */
	ret = SQLGetData(stmt, i, SQL_C_CHAR,
                         buf, sizeof(buf), &indicator);
        if (SQL_SUCCEEDED(ret)) {
            /* Handle null columns */
            if (indicator == SQL_NULL_DATA) strcpy(buf, "NULL");
	    printf("  Column %u : %s\n", i, buf);
        }
    }
  }
}

To compile this code using the unixODBC driver manager installed in /usr/local you would use:

cc -I/usr/local/include exam1.c -o exam1 \
   -L/usr/local/lib -lodbc

When run it produces output like:

Row 0
  Column 1 : easysoft
  Column 2 : dbo
  Column 3 : activeproductcodes
  Column 4 : TABLE
  Column 5 : NULL
Row 1
  Column 1 : easysoft
  Column 2 : dbo
  Column 3 : products
  Column 4 : TABLE
  Column 5 : NULL

For more about fetching results, see ODBC from C Tutorial Part 2 - Fetching Results

Handling and Reporting ODBC Errors and Diagnostics

Return Statuses

All ODBC APIs return a status value which may be used to check whether the function succeeded or not.

In C you can test the return value from an ODBC function using the macro SQL_SUCCEEDED e.g.

SQLRETURN fsts;
/* Assume for this example the environment has already been allocated */
SQLHENV   envh;
SQLHDBC   dbch;

fsts = SQLAllocHandle(SQL_HANDLE_DBC, envh, &dbch);
if (!SQL_SUCCEEDED(fsts))
{
  /* an error occurred allocating the database handle */
}
else
{
  /* Database handle allocated OK */
}

The macro SQL_SUCCEEDED is defined as:

#define SQL_SUCCEEDED(rc) (((rc)&(~1))==0)

Virtually all ODBC functions can return two values which indicate success

Both of these returns cause the SQL_SUCCEEDED macro to result in 1. If a function returns SQL_SUCCESS_WITH_INFO it means that the call succeeded but an informational message was produced.

e.g. with some drivers you might set the cursor type, prepare a statement and then execute it. When SQLExecute is called the statement is acted upon but the driver might change the cursor type to something else. In this case, SQLExecute would return SQL_SUCCESS_WITH_INFO and the driver would add a diagnostic indicating the cursor type had been changed.

Another example is SQLGetData which can return SQL_SUCCESS_WITH_INFO to indicate the buffer you supplied for the column data was not big enough and the data returned has been truncated.

You should note that a few ODBC functions return a status which fails the SQL_SUCCEEDED macro but do not indicate an error as such. e.g. SQLFetch can return SQL_NO_DATA indicating there is no further rows in the result set, this is not necessarily an error.

Retrieving ODBC Diagnostics

When an ODBC function returns an error or SQL_SUCCESS_WITH_INFO then the driver will associate a diagnostic with the handle used in the ODBC call. You can obtain the diagnostic to find out what failed by calling SQLGetDiagRec with the handle you used in the ODBC call that failed.

The driver may associate multiple diagnostic records with a handle. You can call SQLGetDiagField and request the SQL_DIAG_NUMBER attribute to find out how many diagnostics exist. Alternatively, as diagnostic records start at 1, you can repeatedly call SQLGetDiagRec asking for record 1, then 2 (and so on) until SQLGetDiagRec returns SQL_NO_DATA.

As, an example, the following C function takes a function name string, handle type and handle and retrieves all the diagnostics associated with that handle.

void extract_error(
    char *fn,
    SQLHANDLE handle,
    SQLSMALLINT type)
{
    SQLINTEGER	 i = 0;
    SQLINTEGER	 native;
    SQLCHAR	 state[ 7 ];
    SQLCHAR	 text[256];
    SQLSMALLINT	 len;
    SQLRETURN	 ret;

    fprintf(stderr,
            "\n"
            "The driver reported the following diagnostics whilst running "
            "%s\n\n",
            fn);

    do
    {
        ret = SQLGetDiagRec(type, handle, ++i, state, &native, text,
                            sizeof(text), &len );
        if (SQL_SUCCEEDED(ret))
            printf("%s:%ld:%ld:%s\n", state, i, native, text);
    }
    while( ret == SQL_SUCCESS );
}

Using the example above which attempts to allocate a database handle you could use extract_error as follows:

SQLRETURN fsts;
/* Assume for this example the environment has already been allocated */
SQLHENV   envh;
SQLHDBC   dbch;

fsts = SQLAllocHandle(SQL_HANDLE_DBC, envh, &dbch);
if (!SQL_SUCCEEDED(fsts))
{
  extract_error("SQLAllocHandle for dbc", envh, SQL_HANDLE_ENV);
  exit(1);
}
else
{
  /* Database handle allocated OK */
}

ODBC 2.0 applications will use SQLError instead of SQLGetDiagRec.

Diagnostic Fields

When you call SQLGetDiagRec you can retrieve 3 diagnostic fields:

The state is a five character SQLSTATE code. The first two characters indicate the class and the next three indicate the subclass. SQLSTATEs provide detailed information about the cause of a warning or error. You can look states up in the ODBC specification.

The native error code is a code specific to the data source. This number is often extremely useful to the driver developers in locating an internal error or state. If you are reporting a bug in the OOB ODBC driver for which you obtained an error you should always quote the ODBC function called, the error text and this native number.

The message text is the text of the diagnostic. This string takes one of two forms:

For errors and warnings that do not occur in a data source the format is:

[vendor-identifier][ODBC-component-identifier]component-supplied-text

otherwise it is:

[vendor-identifier][ODBC-component-identifier][data-source-identifer]
	data-source-supplied-text

Appendix A: Resources

Article Feedback

* Did this content help you?
* Please select one option based on your first choice:

(* Required Fields)