TASKINDEX adaptor: ODBC


The ODBC taskindex adaptor is the first database-oriented work I'm doing. This isn't any secret initiative on the part of Microsoft, it's just that my desktop and laptop both run Windows (long habit and it's not going to change soon) and so whipping up an Access database is a quick way for me to do prototyping. As soon as this works, I'm going to go over to my Solaris box and do a PostgreSQL adaptor, really.

Anyway, this is the second taskindex adaptor I'm writing; the first is the simple stdout notification-only indexer. This is the first which will be searchable.
 
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <io.h>
#include <errno.h>
#include <windows.h> /* Boy, *this* feels weird! */
#include <sql.h>
#include <sqlext.h>
#include <sqltypes.h>
#include "xmlapi.h"
#include "../wftk_internals.h"
The adaptor_info structure is used to pass adaptor info (duh) back to the config module when it's building an adaptor instance. Here's what it contains:
 
static char *names[] = 
{
   "init",
   "free",
   "info",
   "procnew",
   "procdel",
   "procget",
   "procput",
   "proclist",
   "proccomplete",
   "procerror",
   "tasknew",
   "taskdel",
   "taskget",
   "taskput",
   "tasklist",
   "taskcomplete",
   "taskreject",
   "reqnew",
   "reqdel",
   "reqget",
   "reqput",
   "reqlist",
   "reqaccept",
   "reqdecline"
};

XML * TASKINDEX_odbc_init (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_free (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_info (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_procnew (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_procdel (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_procget (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_procput (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_proclist (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_proccomplete (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_procerror (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_tasknew (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_taskdel (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_taskget (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_taskput (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_tasklist (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_taskcomplete (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_taskreject (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_reqnew (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_reqdel (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_reqget (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_reqput (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_reqlist (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_reqaccept (WFTK_ADAPTOR * ad, va_list args);
XML * TASKINDEX_odbc_reqdecline (WFTK_ADAPTOR * ad, va_list args);

static WFTK_API_FUNC vtab[] = 
{
   TASKINDEX_odbc_init,
   TASKINDEX_odbc_free,
   TASKINDEX_odbc_info,
   TASKINDEX_odbc_procnew,
   TASKINDEX_odbc_procdel,
   TASKINDEX_odbc_procget,
   TASKINDEX_odbc_procput,
   TASKINDEX_odbc_proclist,
   TASKINDEX_odbc_proccomplete,
   TASKINDEX_odbc_procerror,
   TASKINDEX_odbc_tasknew,
   TASKINDEX_odbc_taskdel,
   TASKINDEX_odbc_taskget,
   TASKINDEX_odbc_taskput,
   TASKINDEX_odbc_tasklist,
   TASKINDEX_odbc_taskcomplete,
   TASKINDEX_odbc_taskreject,
   TASKINDEX_odbc_reqnew,
   TASKINDEX_odbc_reqdel,
   TASKINDEX_odbc_reqget,
   TASKINDEX_odbc_reqput,
   TASKINDEX_odbc_reqlist,
   TASKINDEX_odbc_reqaccept,
   TASKINDEX_odbc_reqdecline
};

static struct adaptor_info _TASKINDEX_odbc_info =
{
   24,
   names,
   vtab
};
Cool. So here's the incredibly complex function which returns a pointer to that:
 
struct adaptor_info * TASKINDEX_odbc_get_info ()
{
   return & _TASKINDEX_odbc_info;
}
Thus concludes the communication with the config module. Now on with the actual implementation of functionality. Initialization of an ODBC connection isn't brain surgery, but it's not trivial, either. In fact, this is the first adaptor which needs to stash binary information somewhere. So I added the bindata member of the WFTK_ADAPTOR structure.
 
struct _TASKINDEX_odbc_handles {
   SQLHENV henv;
   SQLHDBC hdbc;
   int ok;
};
XML * TASKINDEX_odbc_init (WFTK_ADAPTOR * ad, va_list args) {
   SQLRETURN rslt;
   const char * parms;
   char spec[256];
   char buf[1024];
   SQLSMALLINT junk;
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) malloc (sizeof (struct _TASKINDEX_odbc_handles));

   if (!handles) {
      xml_set (ad->parms, "error", "Unable to allocate binary stash.");
      return (XML *) 0;
   }
   ad->bindata = (void *) handles;
   handles->henv = SQL_NULL_HANDLE;
   handles->hdbc = SQL_NULL_HANDLE;
   handles->ok = 0;

   parms = xml_attrval (ad->parms, "parm");
   if (!*parms) parms = config_get_value (ad->session, "taskindex.odbc.default");
   if (!*parms) {
      xml_set (ad->parms, "error", "No connection specified and no default connection configured.");
      return (XML *) 0;
   }

   strcpy (spec, "odbc:");
   strcat (spec, parms);
   xml_set (ad->parms, "spec", spec);
   if (strchr (parms, '=')) {
      xml_set (ad->parms, "conn", parms);
   } else {
      sprintf (buf, "taskindex.odbc.%s.conn", parms);
      xml_set (ad->parms, "conn", config_get_value (ad->session, buf));
   }

   /* OK, let's connect to this database specified. */
   rslt = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &(handles->henv));  /* My first C ODBC statement ever! */
   if (rslt == SQL_ERROR) {
      xml_set (ad->parms, "error", "Unable to allocate ODBC environment handle.");
      return (XML *) 0;
   }
   SQLSetEnvAttr (handles->henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER) SQL_OV_ODBC2, 0);

   rslt = SQLAllocHandle (SQL_HANDLE_DBC, handles->henv, &(handles->hdbc));
   if (rslt == SQL_ERROR) {
      xml_set (ad->parms, "error", "Unable to allocate ODBC connection handle.");
      return (XML *) 0;
   }

   rslt = SQLDriverConnect (handles->hdbc, NULL, (char *) xml_attrval (ad->parms, "conn"), (SQLSMALLINT) strlen (xml_attrval (ad->parms, "conn")), buf, (SQLSMALLINT) sizeof(buf), &junk, SQL_DRIVER_NOPROMPT);
   if (rslt == SQL_ERROR) {
      xml_set (ad->parms, "error", "Unable to connect to database specified.");
      return (XML *) 0;
   }

   handles->ok = 1;   
   return (XML *) 0;
}
Freeing the ODBC adaptor is the first significant freeing function I've yet written in the wftk.
 
XML * TASKINDEX_odbc_free (WFTK_ADAPTOR * ad, va_list args) {
   struct _TASKINDEX_odbc_handles * handles = (struct _TASKINDEX_odbc_handles *) (ad->bindata);
   if (handles) {
      SQLDisconnect (handles->hdbc);
      SQLFreeHandle (SQL_HANDLE_DBC, handles->hdbc);
      SQLFreeHandle (SQL_HANDLE_ENV, handles->henv);
      free (handles);
      ad->bindata = (void *) 0;
   }
   return (XML *) 0;
}
Info is also straightforward, as always.
 
XML * TASKINDEX_odbc_info (WFTK_ADAPTOR * ad, va_list args) {
   XML * info;

   info = xml_create ("info");
   xml_set (info, "type", "taskindex");
   xml_set (info, "name", "odbc");
   xml_set (info, "ver", "1.0.0");
   xml_set (info, "compiled", __TIMESTAMP__);
   xml_set (info, "author", "Michael Roberts");
   xml_set (info, "contact", "wftk@vivtek.com");
   xml_set (info, "extra_functions", "0");

   return (info);
}
So now we just work our way down the list of functions and basically execute a simple SQL query for each one. The searches will be a little more complicated than that, as they'll have to build XML for the result sets, but that's the basic idea here.

Before we get started, here's a little function that will build an error message using the ODBC SQLGetDiagRec call.
 
void _TASKINDEX_odbc_builderrmsg (XML * ap, SQLHSTMT stmt)
{
   char buf[1024];
   char msg[128];
   SQLSMALLINT len;
   char code[16];
   SQLINTEGER native;

   SQLGetDiagRec (SQL_HANDLE_STMT, stmt, 1, code, &native, msg, sizeof(msg), &len);
   sprintf (buf, "%s[%s %d] %s", xml_attrval (ap, "error"), code, native, msg);
   xml_set (ap, "error", buf);
}
OK. Here we go with actual function implementations.
 
XML * TASKINDEX_odbc_procnew (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * datasheet = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   char * id;      int id_len;
   char * label;   int label_len;
   char * status;  int status_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) datasheet = va_arg (args, XML *);
   if (!datasheet) {
      xml_set (ad->parms, "error", "No process given.");
      return (XML *) 0;
   }

   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      id = (char *) xml_attrval (datasheet, "id");
      id_len = strlen (id);
      label = (char *) xml_attrval (datasheet, "label");
      label_len = strlen (label);
      status = (char *) xml_attrval (datasheet, "status");
      if (!*status) status = "active"; /* Little sneaky here, eh? */
      status_len = strlen (status);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, label_len, 0, label, label_len, &label_len);
      SQLBindParameter (stmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, status_len, 0, status, status_len, &status_len);

      rslt = SQLExecDirect (stmt, "insert into process (id, title, status, started) values (?, ?, ?, {fn NOW()});", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error inserting process ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_procdel (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   char *id;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   int id_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) id = va_arg (args, char *);
   if (!id) {
      xml_set (ad->parms, "error", "No process given.");
      return (XML *) 0;
   }

   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      id_len = strlen (id);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "delete from process where id=?;", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error deleting process ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_procget (WFTK_ADAPTOR * ad, va_list args) { return (XML *) 0; }
XML * TASKINDEX_odbc_procput (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * datasheet = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   int already_there = 0;
   char * id;      int id_len;
   char * label;   int label_len;
   char * status;  int status_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) datasheet = va_arg (args, XML *);
   if (!datasheet) {
      xml_set (ad->parms, "error", "No process given.");
      return (XML *) 0;
   }

   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt == SQL_ERROR) {
      xml_set (ad->parms, "error", "Unable to check existence of process");
   }
   id = (char *) xml_attrval (datasheet, "id");
   id_len = strlen (id);
   label = (char *) xml_attrval (datasheet, "label");
   label_len = strlen (label);
   status = (char *) xml_attrval (datasheet, "status");
   if (!*status) status = "active"; /* Little sneaky, eh? */
   status_len = strlen (status);

   SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);
   rslt = SQLExecDirect (stmt, "select * from process where id=?;", SQL_NTS);
   if (rslt == SQL_ERROR) {
      xml_set (ad->parms, "error", "SQL error checking process existence ");
      _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
   }
   if (SQL_SUCCESS == SQLFetch (stmt)) already_there = 1;
   SQLFreeHandle (SQL_HANDLE_STMT, stmt);

   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, label_len, 0, label, label_len, &label_len);
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, status_len, 0, status, status_len, &status_len);
      SQLBindParameter (stmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      if (already_there == 0) {
         rslt = SQLExecDirect (stmt, "insert into process (title, status, id, started) values (?, ?, ?, {fn NOW()});", SQL_NTS);
      } else {
         rslt = SQLExecDirect (stmt, "update process set title=?, status=? where id=?;", SQL_NTS);
      }
      if (rslt == SQL_ERROR) {
         if (already_there == 0) {
            xml_set (ad->parms, "error", "SQL error inserting process ");
         } else {
            xml_set (ad->parms, "error", "SQL error updating process ");
         }
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_proclist (WFTK_ADAPTOR * ad, va_list args) {
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * list = (XML *) 0;
   XML * proc;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   char * status;  int status_len;
   char * user;    int user_len;
   char query[1024];
   char column[64];  SQLSMALLINT column_len;
   char value[256];  long value_len;
   SQLUSMALLINT binding = 1;
   SQLSMALLINT numcols;
   SQLSMALLINT col;
   int count = 0;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) list = va_arg (args, XML *);
   if (!list) {
      xml_set (ad->parms, "error", "No list specified.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      strcpy (query, "select * from process where ");

      status = (char *) xml_attrval (list, "status");
      if (*status) {
         status_len = strlen (status);
         /* if (binding > 1) strcat (query, "and "); */
         strcat (query, "status=? ");
         SQLBindParameter (stmt, binding++, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, status_len, 0, status, status_len, &status_len);
      }

      user = (char *) xml_attrval (list, "user");
      if (*user) {
         user_len = strlen (user);
         if (binding > 1) strcat (query, "and ");
         strcat (query, "owner=? ");
         SQLBindParameter (stmt, binding++, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, user_len, 0, user, user_len, &user_len);
      }

      if (*xml_attrval (list, "where")) {
         if (binding > 1) strcat (query, "and ");
         strcat (query, xml_attrval (list, "where"));
         binding++;
      }

      if (binding == 1) {
         strcpy (query, "select * from process");
      }

      rslt = SQLExecDirect (stmt, query, SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error listing processes ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      } else {
         /* Here's where we build the list. */
         SQLNumResultCols (stmt, &numcols);
         while (SQL_SUCCESS == SQLFetch (stmt)) {
            proc = xml_create ("process");
            count++;
            for (col=1; col < numcols; col++) {
               SQLColAttribute (stmt, col, SQL_DESC_NAME, column, sizeof(column), &column_len, NULL);
               strlwr (column);
               if (!strcmp (column, "owner")) strcpy (column, "user");
               SQLGetData (stmt, col, SQL_C_CHAR, value, sizeof(value), &value_len);
               xml_set (proc, column, value);
            }
            xml_append (list, proc);
         }
      }

      xml_setnum (list, "count", count);
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   } else {
      xml_set (ad->parms, "error", "Unable to allocate statement handle.");
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_proccomplete (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   char *id;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   int id_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) id = va_arg (args, char *);
   if (!id) {
      xml_set (ad->parms, "error", "No process given.");
      return (XML *) 0;
   }

   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      id_len = strlen (id);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "update process set status='complete' where id=?;", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error marking process complete ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_procerror (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   char *id;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   int id_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) id = va_arg (args, char *);
   if (!id) {
      xml_set (ad->parms, "error", "No process given.");
      return (XML *) 0;
   }

   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      id_len = strlen (id);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "update process set status='error' where id=?;", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error setting process status to error (ironic) ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
OK, now for tasks. Most of this is cut and pasted from above. Well. Actually the above was cut and pasted from here, but that's a different story, and one I'm not telling right at the moment.
 
XML * TASKINDEX_odbc_tasknew (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   char * process; int process_len;
   char * id;      int id_len;
   char * label;   int label_len;
   char * role;    int role_len;
   char * user;    int user_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) task = va_arg (args, XML *);
   if (!task) {
      xml_set (ad->parms, "error", "No task given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process = (char *) xml_attrval (task, "process");
      process_len = strlen (process);
      id = (char *) xml_attrval (task, "id");
      id_len = strlen (id);
      label = (char *) xml_attrval (task, "label");
      label_len = strlen (label);
      role = (char *) xml_attrval (task, "role");
      role_len = strlen (role);
      user = (char *) xml_attrval (task, "user");
      user_len = strlen (user);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, label_len, 0, label, label_len, &label_len);
      SQLBindParameter (stmt, 4, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, role_len, 0, role, role_len, &role_len);
      SQLBindParameter (stmt, 5, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, user_len, 0, user, user_len, &user_len);

      rslt = SQLExecDirect (stmt, "insert into task (id, process, status, description, role, owner, created) values (?, ?, 'active', ?, ?, ?, {fn NOW()});", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error inserting task ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_taskdel (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;

   char *process;
   char *id;
   int process_len, id_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) process = va_arg (args, char *);
   if (!process) {
      xml_set (ad->parms, "error", "No task given.");
      return (XML *) 0;
   }
   id = va_arg (args, char *);
   if (!id) {
      xml_set (ad->parms, "error", "No task given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process_len = strlen (process);
      id_len = strlen (id);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "delete from task where process=? and id=?;", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error deleting task ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_taskget (WFTK_ADAPTOR * ad, va_list args) { return (XML *) 0; }
XML * TASKINDEX_odbc_taskput (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   char * process; int process_len;
   char * id;      int id_len;
   char * label;   int label_len;
   char * role;    int role_len;
   char * user;    int user_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) task = va_arg (args, XML *);
   if (!task) {
      xml_set (ad->parms, "error", "No task given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process = (char *) xml_attrval (task, "process");
      process_len = strlen (process);
      id = (char *) xml_attrval (task, "id");
      id_len = strlen (id);
      label = (char *) xml_attrval (task, "label");
      label_len = strlen (label);
      role = (char *) xml_attrval (task, "role");
      role_len = strlen (role);
      user = (char *) xml_attrval (task, "user");
      user_len = strlen (user);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, label_len, 0, label, label_len, &label_len);
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, role_len, 0, role, role_len, &role_len);
      SQLBindParameter (stmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, user_len, 0, user, user_len, &user_len);
      SQLBindParameter (stmt, 4, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 5, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "update task set description=?, role=?, owner=? where process=? and id=?", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error inserting task ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_tasklist (WFTK_ADAPTOR * ad, va_list args) {
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * list = (XML *) 0;
   XML * task;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   char * process; int process_len;
   char * status;  int status_len;
   char * role;    int role_len;
   char * user;    int user_len;
   char query[1024];
   char column[64];  SQLSMALLINT column_len;
   char value[256];  long value_len;
   SQLUSMALLINT binding = 1;
   SQLSMALLINT numcols;
   SQLSMALLINT col;
   int count = 0;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) list = va_arg (args, XML *);
   if (!list) {
      xml_set (ad->parms, "error", "No list specified.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      strcpy (query, "select * from task where ");

      process = (char *) xml_attrval (list, "process");
      if (*process) {
         process_len = strlen (process);
         /* if (binding > 1) strcat (query, "and "); */
         strcat (query, "process=? ");
         SQLBindParameter (stmt, binding++, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len);
      }

      status = (char *) xml_attrval (list, "status");
      if (*status) {
         status_len = strlen (status);
         /* if (binding > 1) strcat (query, "and "); */
         strcat (query, "status=? ");
         SQLBindParameter (stmt, binding++, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, status_len, 0, status, status_len, &status_len);
      }

      role = (char *) xml_attrval (list, "role");
      if (*role) {
         role_len = strlen (role);
         if (binding > 1) strcat (query, "and ");
         strcat (query, "role=? ");
         SQLBindParameter (stmt, binding++, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, role_len, 0, role, role_len, &role_len);
      }

      user = (char *) xml_attrval (list, "user");
      if (*user) {
         user_len = strlen (user);
         if (binding > 1) strcat (query, "and ");
         strcat (query, "owner=? ");
         SQLBindParameter (stmt, binding++, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, user_len, 0, user, user_len, &user_len);
      }

      if (*xml_attrval (list, "where")) {
         if (binding > 1) strcat (query, "and ");
         strcat (query, xml_attrval (list, "where"));
         binding++;
      }

      if (binding == 1) {
         strcpy (query, "select * from task");
      }

      rslt = SQLExecDirect (stmt, query, SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error listing tasks ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      } else {
         /* Here's where we build the list. */
         SQLNumResultCols (stmt, &numcols);
         while (SQL_SUCCESS == SQLFetch (stmt)) {
            task = xml_create ("task");
            count++;
            for (col=1; col < numcols; col++) {
               SQLColAttribute (stmt, col, SQL_DESC_NAME, column, sizeof(column), &column_len, NULL);
               strlwr (column);
               if (!strcmp (column, "description")) strcpy (column, "label");
               if (!strcmp (column, "owner")) strcpy (column, "user");
               SQLGetData (stmt, col, SQL_C_CHAR, value, sizeof(value), &value_len);
               xml_set (task, column, value);
            }
            xml_append (list, task);
         }
      }

      xml_setnum (list, "count", count);
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   } else {
      xml_set (ad->parms, "error", "Unable to allocate statement handle.");
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_taskcomplete (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;

   char *process = NULL;
   char *id = NULL;
   int process_len, id_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) process = va_arg (args, char *);
   if (!process) process = "";
   if (args) id = va_arg (args, char *);
   if (!id) {
      xml_set (ad->parms, "error", "No task given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process_len = strlen (process);
      id_len = strlen (id);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "update task set status='complete', complete={fn NOW()} where process=? and id=? and status='active';", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error updating task as complete ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_taskreject (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;

   char *process = NULL;
   char *id = NULL;
   int process_len, id_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) process = va_arg (args, char *);
   if (!process) process = "";
   if (args) id = va_arg (args, char *);
   if (!id) {
      xml_set (ad->parms, "error", "No task given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process_len = strlen (process);
      id_len = strlen (id);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "update task set status='reject' where process=? and id=? and status='active';", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error updating task as complete ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
And then the request stuff. Requests, as far as the task index is concerned, are just funny tasks with state=request. All the good stuff (notifications, workflow, reassignment) requires a datasheet for now. It's imperfect, but it's a start. If a non-datasheet request is accepted, it turns into an active task. If a datasheet request is accepted, or any request is declined, it's just deleted. Pretty simple, actually.
 
XML * TASKINDEX_odbc_reqnew (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   char * process; int process_len;
   char * id;      int id_len;
   char * label;   int label_len;
   char * role;    int role_len;
   char * user;    int user_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) task = va_arg (args, XML *);
   if (!task) {
      xml_set (ad->parms, "error", "No request given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process = (char *) xml_attrval (task, "process");
      process_len = strlen (process);
      id = (char *) xml_attrval (task, "id");
      id_len = strlen (id);
      label = (char *) xml_attrval (task, "label");
      label_len = strlen (label);
      user = (char *) xml_attrval (task, "of");
      user_len = strlen (user);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, label_len, 0, label, label_len, &label_len);
      SQLBindParameter (stmt, 4, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, user_len, 0, user, user_len, &user_len);

      rslt = SQLExecDirect (stmt, "insert into task (id, process, status, description, owner, created) values (?, ?, 'request', ?, ?, {fn NOW()});", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error inserting request ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_reqdel (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;

   char *process = NULL;
   char *id = NULL;
   int process_len, id_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) process = va_arg (args, char *);
   if (!process) process = "";
   if (args) id = va_arg (args, char *);
   if (!id) {
      xml_set (ad->parms, "error", "No request given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process_len = strlen (process);
      id_len = strlen (id);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "delete from task where process=? and id=?;", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error deleting request ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_reqget (WFTK_ADAPTOR * ad, va_list args) { return (XML *) 0; }
XML * TASKINDEX_odbc_reqput (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   char * process; int process_len;
   char * id;      int id_len;
   char * label;   int label_len;
   char * user;    int user_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) task = va_arg (args, XML *);
   if (!task) {
      xml_set (ad->parms, "error", "No request given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process = (char *) xml_attrval (task, "process");
      process_len = strlen (process);
      id = (char *) xml_attrval (task, "id");
      id_len = strlen (id);
      label = (char *) xml_attrval (task, "label");
      label_len = strlen (label);
      user = (char *) xml_attrval (task, "of");
      user_len = strlen (user);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, label_len, 0, label, label_len, &label_len);
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, user_len, 0, user, user_len, &user_len);
      SQLBindParameter (stmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 4, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "update task set description=?, owner=? where process=? and id=?", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error inserting task ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_reqlist (WFTK_ADAPTOR * ad, va_list args) {
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * list = (XML *) 0;
   XML * task;
   SQLHSTMT stmt;
   SQLRETURN rslt;
   char * process; int process_len;
   char * user;    int user_len;
   char query[1024];
   char column[64];  SQLSMALLINT column_len;
   char value[256];  long value_len;
   SQLUSMALLINT binding = 1;
   SQLSMALLINT numcols;
   SQLSMALLINT col;
   int count = 0;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) list = va_arg (args, XML *);
   if (!list) {
      xml_set (ad->parms, "error", "No list specified.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      strcpy (query, "select * from task where status='request' ");

      process = (char *) xml_attrval (list, "process");
      if (*process) {
         process_len = strlen (process);
         strcat (query, "and process=? ");
         SQLBindParameter (stmt, binding++, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len);
      }

      user = (char *) xml_attrval (list, "user");
      if (*user) {
         user_len = strlen (user);
         strcat (query, "and owner=? ");
         SQLBindParameter (stmt, binding++, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, user_len, 0, user, user_len, &user_len);
      }

      if (*xml_attrval (list, "where")) {
         strcat (query, "and ");
         strcat (query, xml_attrval (list, "where"));
         binding++;
      }

      rslt = SQLExecDirect (stmt, query, SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error listing tasks ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      } else {
         /* Here's where we build the list. */
         SQLNumResultCols (stmt, &numcols);
         while (SQL_SUCCESS == SQLFetch (stmt)) {
            task = xml_create ("task");
            count++;
            for (col=1; col < numcols; col++) {
               SQLColAttribute (stmt, col, SQL_DESC_NAME, column, sizeof(column), &column_len, NULL);
               strlwr (column);
               if (!strcmp (column, "description")) strcpy (column, "label");
               if (!strcmp (column, "owner")) strcpy (column, "user");
               SQLGetData (stmt, col, SQL_C_CHAR, value, sizeof(value), &value_len);
               xml_set (task, column, value);
            }
            xml_append (list, task);
         }
      }

      xml_setnum (list, "count", count);
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   } else {
      xml_set (ad->parms, "error", "Unable to allocate statement handle.");
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_reqaccept (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;

   char *process = NULL;
   char *id = NULL;
   int process_len, id_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) process = va_arg (args, char *);
   if (!process) process = "";
   if (args) id = va_arg (args, char *);
   if (!id) {
      xml_set (ad->parms, "error", "No request given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process_len = strlen (process);
      id_len = strlen (id);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      if (*process) {
         rslt = SQLExecDirect (stmt, "delete from task where process=? and id=? and status='request';", SQL_NTS);
      } else {
         rslt = SQLExecDirect (stmt, "update task set status='active' where process=? and id=? and status='request';", SQL_NTS);
      }
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error accepting request ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}
XML * TASKINDEX_odbc_reqdecline (WFTK_ADAPTOR * ad, va_list args)
{
   struct _TASKINDEX_odbc_handles *handles = (struct _TASKINDEX_odbc_handles *) ad->bindata;
   XML * task = (XML *) 0;
   SQLHSTMT stmt;
   SQLRETURN rslt;

   char *process = NULL;
   char *id = NULL;
   int process_len, id_len;

   if (!handles) return (XML *) 0;
   if (!handles->ok) return (XML *) 0;
   if (args) process = va_arg (args, char *);
   if (!process) process = "";
   if (args) id = va_arg (args, char *);
   if (!id) {
      xml_set (ad->parms, "error", "No request given.");
      return (XML *) 0;
   }
   rslt = SQLAllocHandle (SQL_HANDLE_STMT, handles->hdbc, &stmt);
   if (rslt != SQL_ERROR) {
      process_len = strlen (process);
      id_len = strlen (id);

      SQLBindParameter (stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, process_len, 0, process, process_len, &process_len); 
      SQLBindParameter (stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, id_len, 0, id, id_len, &id_len);

      rslt = SQLExecDirect (stmt, "delete task where process=? and id=? and status='request';", SQL_NTS);
      if (rslt == SQL_ERROR) {
         xml_set (ad->parms, "error", "SQL error declining request ");
         _TASKINDEX_odbc_builderrmsg (ad->parms, stmt);
      }
      SQLFreeHandle (SQL_HANDLE_STMT, stmt);
   }
   return (XML *) 0;
}

This code and documentation are released under the terms of the GNU license. They are additionally copyright (c) 2000, Vivtek. All rights reserved except those explicitly granted under the terms of the GNU license. This presentation was prepared with LPML. Try literate programming. You'll like it.