Originally Published: Wednesday, 18 July 2001 Author: Neil Matthew and Richard Stones
Published to: develop_articles/Development Articles Page: 4/4 - [Printable]

Excerpt from Professional Linux Programming: Chapter 4; PostgreSQL Interfacing

In this articles excerpt from the brilliant Professional Linux Programming (Wrox Press) the authors look at two ways of accessing a PostgreSQL database from C code. Firstly with a conventional library based method, then at how SQL can be embedded more directly into C code. The excerpt also contains major parts of a complete application. Linux.com wants to thank Wrox Press for making this valuable educational material available to the Linux.com community!

  << Page 4 of 4  

Accessing the retrieved data

Last, but certainly not least, we need to access the data we have retrieved. As we mentioned before, type information of the data being returned is not available in any sensible fashion, so you may be wondering how we are going to manage this in code. The answer is very simple - libpq always returns a string representation of the returned data, which we can convert ourselves. (Actually this isn't quite true, for BINARY cursors binary data is returned, but very few users will need such advanced PostgreSQL features.)

What we can discover is the length of the representation of the data that will be returned when we fetch the data, this is done with PQgetlength:

int PQgetlength(PGresult *result, int tuple_number, int
        field_index);

Notice that this has a tuple_number field, which you will recall is PostgreSQL speak for a row. This is because we might have not used a cursor (as we saw earlier) and retrieved all the data in one go, or asked for more than one row at a time, as we did in the last example. Without this parameter, retrieving several rows at once would have been pointless, since we could not have accessed the data in any but the last row retrieved.

We get the string representation of the data with PQgetvalue:

char *PQgetvalue(PGresult *result, int tuple_number, int
        field_index);

This returns a NULL terminated string. The actual string is inside a PGresult structure, so you must copy the data out if you want it accessible after doing anything else with the result structure. At this point the astute amongst you may have spotted a snag - how do you distinguish between an empty string being returned because the string in the database had no length, and an empty string being returned because the database column was a NULL value (which we're sure you remember means 'unknown', rather than empty). The answer is a special function, PQgetisnull, which is used to separate the two database values:

int PQgetisnull(PGresult *result, int tuple_number, int
        field_index);

This returns 1 if the field was NULL in the database, otherwise 0.

Now, at last, we are in a position to write our final version of our test code, which returns data from the database row by row, displaying the column information and data as it goes. Before we run this, we set one of the rows we will retrieve to have a NULL value, so we can check our code detects NULLs correctly. Depending on the data you put into the children table, you may have to use a different childno. I had a childno of 9, with an age of 1, where we set the fname field to NULL, by executing this statement in psql:

UPDATE children set fname = NULL where childno = 9;

Now here is the final version of our SELECT from C code, sel7.c. The principal changes are highlighted, and some 'debug' type lines have also been removed, in order to clean up the output a little:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <libpq-fe.h>
PGconn *conn = NULL;
void tidyup_and_exit();
int execute_one_statement(const char *stmt_to_exec,
    PGresult **result);
void show_column_info(PGresult *result);
void show_one_row_data(PGresult *result);
int main()
{
PGresult *result;
int stmt_ok;
char *connection_str = "host=gw1 dbname=rick";
FILE *output_stream;
PQprintOpt print_options;
conn = PQconnectdb(connection_str);
if (PQstatus(conn) == CONNECTION_BAD) {
fprintf(stderr, "Connection to %s failed, %s",
 connection_str,
PQerrorMessage(conn));
tidyup_and_exit();
} else {
printf("Connected OK\n");
}
stmt_ok = execute_one_statement("BEGIN WORK",
  &result);
if (stmt_ok) {
PQclear(result);
stmt_ok = execute_one_statement("DECLARE
 age_fname_cursor CURSOR FOR
SELECT age, fname FROM children WHERE
age < '6'", &result);
if (stmt_ok) {
PQclear(result);
stmt_ok = execute_one_statement("FETCH 1 IN
   age_fname_cursor",
&result);
if (stmt_ok) show_column_info(result);
while(stmt_ok && PQntuples(result) > 0) {
show_one_row_data(result);
PQclear(result);
stmt_ok = execute_one_statement("FETCH NEXT IN
   age_fname_cursor",
&result);
}
stmt_ok = execute_one_statement("COMMIT WORK",
  &result);
}
}
if (stmt_ok) PQclear(result);
PQfinish(conn);
return EXIT_SUCCESS;
}

int execute_one_statement(const char *stmt_to_exec,
    PGresult **res_ptr) {
int retcode = 1;
const char *str_res;
PGresult *local_result;
printf("About to execute %s\n", stmt_to_exec);
local_result = PQexec(conn, stmt_to_exec);
*res_ptr = local_result;
if (!local_result) {
printf("PQexec command failed, no error code\n");
retcode = 0;
} else {
switch (PQresultStatus(local_result)) {
case PGRES_COMMAND_OK:
str_res = PQcmdTuples(local_result);
if (strlen(str_res) > 0) {
printf("Command executed OK, %s rows affected\n",
    str_res);
} else {
printf("Command executed OK, no rows affected\n");
}
break;
case PGRES_TUPLES_OK:
printf("Select executed OK, %d rows found\n",
    PQntuples(local_result));
break;
default:
printf("Command failed with code %s, error message
   %s\n",
PQresStatus(PQresultStatus(local_result)),
PQresultErrorMessage(local_result));
PQclear(local_result);
retcode = 0;
break;
}
}
return retcode;
} /* execute_one_statement */
void show_column_info(PGresult *result) {
int num_columns = 0;
int i;
if (!result) return;
num_columns = PQnfields(result);
printf("%d columns in the result set\n",
   num_columns);
for(i = 0; i < num_columns; i++) {
printf("Field %d, Name %s, Internal size %d\n",
i,
PQfname(result, i),
PQfsize(result, i));
}
} /* show_column_info */

void show_one_row_data(PGresult *result) {
int col;
for(col = 0; col < PQnfields(result); col++) {
printf("DATA: %s\n", PQgetisnull(result, 0, col) ?
   "<NULL>": PQgetvalue(result, 0, col));
}
} /* show_one_row_data */
void tidyup_and_exit() {
if (conn != NULL) PQfinish(conn);
exit(EXIT_FAILURE
}
Notice we check for NULLs in all columns. When we run this, we get:
Connected OK
2 columns in the result set
Field 0, Name age, Internal size 4
Field 1, Name fname, Internal size -1
DATA: 4
DATA: Adrian
DATA: 4
DATA: Allen
DATA: 1
DATA: <NULL>

And that concludes our tour of the libpq library. We have seen how we can use the libpq library to access data in the database, retrieving it row by row using cursors. We have also seen how to extract column information, and handle NULL values in the database.

ECPG

Now it's time to look at the alternative way of combining SQL and C, by embedding SQL statements in the C code, and then pre-processing them into something the C compiler can understand, before invoking the C compiler. There is still a library to interface C calls to the database, but the details are hidden away behind a pre-processor.

PostgreSQL's ecpg follows the ANSI standard for embedding SQL in C code, and what follows will be familiar to programmers who have used systems such as Oracle's PRO*C or Informix's ESQL-C. At the time of writing some of the less used features of embedded SQL are not supported, and the standard documentation for ecpg that ships with PostgreSQL is somewhat limited.

Since we have now worked through many of the basics of SQL, this section will actually be quite short. The first problem that has to be tackled is how to delimit sections in the file that the ecpg pre-processor needs to process. This is done with the special sequence in the source that starts 'exec sql', then contains the SQL you want to execute, and ends with a ';'. Depending on the exact syntax, as we shall see in a moment, this can either be a single line that needs to be processed, or it can be used to mark a section that needs pre-processing.

If we want to write a simple C program that performs a single UPDATE statement in the middle of some C code, we need to do only one thing in the source code - embed the UPDATE SQL statement.

What could be easier? Let's write a very simple C program with some embedded SQL that updates a table. By convention these have a file extension of pgc. Here is upd1.pgc:

#include <stdlib.h>
exec sql include sqlca;
main() {
exec sql connect to 'rick@gw1';
exec sql BEGIN WORK;
exec sql UPDATE children SET fname = 'Gavin' WHERE
  childno = 9;
exec sql COMMIT WORK;
exec sql disconnect all;
return EXIT_SUCCESS;
}

At first sight, this hardly looks like C at all. However, if you ignore the lines that start exec sql, you can see it is just a minimal C program. To compile this program we need a two-stage process. First we must run the ecpg pre-processor, then we compile the resulting C file, linking it with the ecpg library. To compile this you may need to add a -I optionto ecpg, to tell it where to look for the include file, depending on your installation. For this program, upd1.pgc, the commands are:

$ ecpg -t -I/usr/include/pgsql upd1.pgc
$ gcc -o upd1 -I/usr/include/pgsql upd1.c -lecpg -lpq

The ecpg command pre-processes the file, leaving a .c file, which we then compile in the normal way, linking with two PostgreSQL libraries. The '-t' on the command line for ecpg tells ecpg that we wish to manage our own transactions with explicit BEGIN WORK and COMMIT WORK statements in the source file. By default ecpg will automatically start a transaction when you connect to the database. There is nothing wrong with this, it's just that the authors prefer to explicitly define transaction blocks.

You will notice the connect string is 'rick@gw1'. This requests a connection to the database 'rick' on server 'gw1'. No password is needed since that's a local machine, and I am already logged in as user rick. However in the general case you can specify the connection in a URL style format, in which case the format is

        <protocol>:<service>://<machine>:<port>/<dbname>
        as <connection name> as <login name> using
        <password for login>

A concrete example makes this much clearer. Suppose we want to connect using tcp to the postgresql service on the dbs6 machine, port 5432, connecting to the database rick, using the database login name neil, who has a password secret. The connect line we would put in our programwould be:

exec sql connect to tcp:postgresql://dbs6:5432/rick as
 connect_2 user neil using secret;

If we want to separate out the different elements, then we can use the same style of connect request, but using ''host variables'', which you will notice always start with a ':'. We will see more about host variables later in the chapter; for now just imagine them as normal C variables.

exec sql BEGIN DECLARE SECTION;
char connect_str[256];
char as_str[25];
char user_str[25];
char using_str[25];
exec sql END DECLARE SECTION;
strcpy(connect_str,
 "tcp:postgresql://localhost:5432/rick");
strcpy(as_str, "connect_2");
strcpy(user_str, "neil");
strcpy(using_str, "secret");
exec sql connect to :connect_str as :as_str user
   :user_str using :using_str ;
if (sqlca.sqlcode != 0) {
pg_print_debug(__FILE__, __LINE__, sqlca, "Connect
 failed");
return DVD_ERR_BAD_DATABASE;
}

Now we have seen the basics, let's look in slightly more detail at what ecpg does.

The first feature that we almost always need when writing an ecpg program is to include a header file that gives us access to errors and status information from PostgreSQL. Since we need this file to be pre-processed by the ecpg processor, before the C compiler runs, a normal include will not do. What we need is to use the exec sql include command. Since there is just a single file called sqlca, which we almost always need to include, pgc files usually start with:

exec sql include sqlca;

This causes the ecpg command to include the file sqlca.h, which is (by default) found in the /usr/include/pgsql directory, though depending on your installation this may of course be different. This important include file declares an sqlca structure, and variable of the same name, that allows us to determine results from our SQL statements. The sqlca structure is a standard structure used when embedding SQL in C code, though implementations vary slightly. For our install of PostgreSQL the structure is declared to be:

struct sqlca
{
char sqlcaid[8];
long sqlabc;
long sqlcode;
struct
{
int sqlerrml;
char sqlerrmc[70];
} sqlerrm;
char sqlerrp[8];
long sqlerrd[6];
char sqlwarn[8];
char sqlext[8];
};

Actually interpreting the contents of sqlca can seem a little odd. The implementation of ecpg that comes with PostgreSQL does not implement as much of the sqlca functionality as some commercial packages such as Oracle. This means some members of the structure are unused, however all the important functions are implemented, so it is perfectly usable.

When processing an sqlca structure you first need to check sqlca.sqlcode. If it is less than zero then something serious went wrong, if it's zero all is well, and if it's 100 then no data was found, but that was not an error.

When an INSERT, UPDATE or SELECT statement succeeds, sqlca.sqlerrd[2]will contain the number of rows that were affected.

If sqlca.sqlwarn[0] is 'W', then a minor error occurred, usually data was retrieved successfully, but was not transferred correctly into a host variable (we will meet these later in the chapter).

When an error occurs sqlca.sqlerrm.sqlerrmc contains a string describing the error.

Commercial packages use more fields, that can tell you a notional 'cost' and other information, but these are not currently supported in PostgreSQL. However since such information is only occasionally useful, it's omission is not generally missed.

Let's just summarize that explanation:

sqlca.sqlcode

Contains a negative value for serious errors, zero for success, 100 for no data found.

sqlca.sqlerrm.sqlerrmc

Contains a textual error message.

sqlca.sqlerrd[2]

Contains the number of rows affected.

sqlca.sqlwarn[0]

Is set to 'W' when data was retrieved, but not correctly transferred to the program.

Let's try this out, by modifying our upd1.pgc file to include sqlca, and also deliberately making it fail, by using an invalid table name:

#include <stdlib.h>
#include <stdio.h>
exec sql include sqlca;
main() {
exec sql connect to 'rick@gw1';
exec sql BEGIN WORK;
exec sql UPDATE XXchildren SET fname = 'Emma' WHERE age
 = 0;
printf("error code %d, message %s, rows %d, warning
 %c\n", sqlca.sqlcode,
sqlca.sqlerrm.sqlerrmc, sqlca.sqlerrd[2],
  sqlca.sqlwarn[0]);
exec sql COMMIT WORK;
exec sql disconnect all;
return EXIT_SUCCESS;
}

This is upd2.pgc. The highlighted lines show the important changes. Compile it as before:

$ ecpg -t -I/usr/include/pgsql upd2.pgc
$ gcc -g -o upd2 -I /usr/include/pgsql/ upd2.c -lecpg -lpq

This time when we run it, an error is generated:

error code -400, message Postgres error: ERROR:
   xxchildren: Table does not exist.
line 10., rows 0, warning

As you can see, it's a little basic but does the job.

Now we have seen the basics, we can get to important issue - how do we access data that SQL statements embedded in .pgc files return?

The answer is actually quite simple, and relies on variables called host variables, which are accessible to both the statements delimited by exec sql ... ; and to the ordinary C compiler.

We do this by having a declare section, usually near the start of the file, that is processed by both the ecpg processor, and the C compiler. This is achieved by declaring C variables inside a special declare section, which also tells the ecpg processor to process them. We use the delimiting statements:

exec sql begin declare section;

and

exec sql end declare section;

Suppose we wanted to declare two variables, child_name and child_age, that are intended to be accessible in both the embedded SQL and in the Ccode for use in the rest of the program. What we need is:

exec sql begin declare section;
int child_age;
VARCHAR child_name[50];
exec sql end declare section;

You will notice two odd things here, firstly the 'magic number' 50 as a string length, and secondly that VARCHAR is not a normal C type. We are forced to use literal numbers here, because this section of code is being processed by ecpg before the C compiler runs, so it is not possible to use either a #define or a constant. The reason for VARCHAR is because the SQL type of the fname column in children is not a type that maps directly to a C type. We must use the PostgreSQL type in our declaration, which is then converted into a legal C structure by the ecpg pre-processor, before the C compiler sees it. The result of this line in the source file is to create a structure called child_name, with two members, a char array 'arr', and an integer len, to store the length. So what the C compiler sees from this one line is actually:

struct varchar_child_name {int len; char arr[50];}
        child_name;

Now we have two variables, visible both in SQL and in C. We use a slight extension of the SQL syntax, the 'into' keyword, to retrieve data from the table into named variables, which are denoted by having a ':' prepended to the name. This is so they cannot be confused with values or table names. Notice this 'into' is not the same as the extension some vendors support to allow interactive selecting of data from one table into another. The 'into' keyword has a slightly different meaning when using embedded SQL.

exec sql SELECT fname into :child_name FROM children
        WHERE age = :child_age;

The epgc pre-processor converts this to C, which we compile in the normal way. So our complete code is now in selp1.pgc, and looks like this:

#include <stdlib.h>
#include <stdio.h>
exec sql include sqlca;
exec sql begin declare section;
int child_age;
VARCHAR child_name[50];
exec sql end declare section;
main() {
exec sql connect to 'rick@gw1';
exec sql BEGIN WORK;
child_age = 14;
exec sql SELECT fname into :child_name FROM children
    WHERE age = :child_age;
printf("error code %d, message %s, rows %d, warning
  %c\n", sqlca.sqlcode,
sqlca.sqlerrm.sqlerrmc, sqlca.sqlerrd[2],
  sqlca.sqlwarn[0]);
if (sqlca.sqlcode == 0) {
printf("Child's name was %s\n", child_name.arr);
}
exec sql COMMIT WORK;
exec sql disconnect all;
return EXIT_SUCCESS;
}

The important changes are highlighted. Notice we need to use child_name.arr to access the returned data. However you only need to use VARCHAR declarations when you want to get data out of the database - when you want to store data into a VARCHAR field you should use a NULL terminated C string in the normal way.

However there is a potential problem with this program. You will see that we had to declare our child_name VARCHAR to be a fixed size, even though we could not know in advance how large the answer might have been. What will happen if we make child_name only 3 long, and the name stored in the database is longer than this? In this case ecpg will only retrieve the first 3 characters, and will set the warning flag. If we change the declaration to VARCHAR child_name[3] and run the program we get:

error code 0, message , rows 1, warning W
Child's name was Jen

(You may also see some corruption, we will explain why in a moment.)

As you can see, the sqlca.sqlwarn[0] warn character was set to 'W', and the returned name truncated. However since our declaration of child_name is translated into a structure containing a character array of exactly3 characters, there is no location for the string terminator to be stored=2E It's lucky our printout worked at all, though we could have been decidedly cleverer with the printf format string. To be certain of getting aVARCHAR into a normal C string we should always check that sqlca.sqlwarn[0] is not set, and then copy the string away to a separate location, adding the NULL terminator explicitly. A more secure version of the program is selp3.c, which has the following changes:

#include <stdlib.h>
#include <stdio.h>
exec sql include sqlca;
exec sql begin declare section;
int child_age;
VARCHAR child_name[50];
exec sql end declare section;
main() {
exec sql connect to 'rick@gw1';
exec sql BEGIN WORK;
child_age = 14;
exec sql SELECT fname into :child_name FROM children
    WHERE age = :child_age;
printf("error code %d, message %s, rows %d, warning
  %c\n", sqlca.sqlcode,
sqlca.sqlerrm.sqlerrmc, sqlca.sqlerrd[2],
  sqlca.sqlwarn[0]);
if (sqlca.sqlcode == 0) {
child_name.arr[sizeof(child_name.arr) -1] = '\0';
printf("Child's name was %s\n", child_name.arr);
}
exec sql COMMIT WORK;
exec sql disconnect all;
return EXIT_SUCCESS;
}

Now we can retrieve data, it's time to see how we use cursors with ecpg where we want to specify, at run time, the condition for the SELECT, and also retrieve data into C variables. Unlike the libpq example, ecpg, (at least in the version used while writing this chapter), required an explicit OPEN statement to open the cursor, before data could be fetched. This example is selp4.pgc, it's noticeably shorter than the libpq equivalent:

 

#include <stdlib.h>
#include <stdio.h>
exec sql include sqlca;
exec sql begin declare section;
int child_age;
VARCHAR child_name[50];
int req_age;
exec sql end declare section;
main() {
exec sql connect to 'rick@gw1';
exec sql BEGIN WORK;
req_age = 6;
exec sql DECLARE mycursor CURSOR FOR SELECT age, fname
 FROM children
WHERE age > :req_age;
exec sql OPEN mycursor;
exec sql FETCH NEXT IN mycursor into :child_age,
    :child_name;
if (sqlca.sqlcode < 0)
printf("error code %d, message %s, rows %d, warning
    %c\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc,
   sqlca.sqlerrd[2], sqlca.sqlwarn[0]);
while (sqlca.sqlcode == 0) {
if (sqlca.sqlcode >= 0) {
child_name.arr[sizeof(child_name.arr) -1] = '\0';
printf("Child's name and age %s, %d\n", child_name.arr,
child_age);
}
exec sql FETCH NEXT IN mycursor into :child_age,
    :child_name;
if (sqlca.sqlcode < 0) printf("error code %d, message
    %s, rows %d, warning %c\n", sqlca.sqlcode,
  sqlca.sqlerrm.sqlerrmc, sqlca.sqlerrd[2],
   sqlca.sqlwarn[0]);
}
exec sql CLOSE mycursor;
exec sql COMMIT WORK;
exec sql disconnect all;
return EXIT_SUCCESS;
}

When we run this, we get the expected output:

Child's name and age Andrew, 10
Child's name and age Jenny, 14
Child's name and age Alex, 11

You may be thinking that all this messing with VARCHARS is a bit pointless, and providing your strings are known to be reasonably consistent in size, it would be much easier to use fixed length strings. Unfortunately this gives rise to a different problem - PostgreSQL does not store the \0 in CHAR columns. What it does do is fill the field to the maximum size with spaces. So if you store "Foo" in a CHAR(10), when you get the data back you actually get "Foo '', and you have to strip the spaces yourself. It does however add a \0 when you retrieve the string, so you do get a conventional C string returned to you.

There is one last ecpg feature we need to look at, how to detect NULL values. Doing this in ecpg (and indeed the standard way for embedded SQL) is slightly more complex than in libpq, but it's not difficult. Remembering that NULL means unknown, it's clear we can't use a magic string, or special integer value to show NULL, since any of these values could actually occur in the database.

What we have to do is to declare an extra variable, often called an indicator variable, that goes alongside the variable we will use to retrieve the data. This additional indicator variable is set to indicate if the data value retrieved was actually NULL in the database. These are often named ind_nameofrealvariable, or sometimes nameofrealvariable _ind, but could have any name. They are always integers - a negative value indicating that the associated variable has a NULL value.

For example, suppose in our earlier example we needed to detect if age was NULL. What we would do is declare an extra variable in the declare section like this:

int ind_child_age;

Then when we do the FETCH from the cursor, we specify both the real variable, and the indicator variable, joined by a colon, like this:

exec sql FETCH NEXT IN mycursor into
        :child_age:ind_child_age, :child_name;

Then if ind_child_age is not negative, we know that child_age is correctly filled in - otherwise the data in it is not valid because the database value was a NULL. For our final example of ecpg, let's convert our example so it correctly detects NULL values.

First we update our 'children' table, so we have examples of both NULL ages and fnames. The test data we start with looks like this:

SELECT * from children;
childno|fname |age
-------+------+---
1|Andrew| 10
2|Jenny | 14
3|Alex | 11
4|Adrian| 5
19| | 17
16|Emma | 0
18|TBD |
20|Gavin | 4
(8 rows)

As you can see, we have a seventeen year old with an unknown name, and an unborn child whose name is still to be decided, and doesn't have an age yet.

This is selp5.pgc. By way of example, we have also used the alternate form of connection string.

#include <stdlib.h>
#include <stdio.h>
exec sql include sqlca;
exec sql begin declare section;
int child_age;
int ind_child_age;
VARCHAR child_name[50];
int ind_child_name;
exec sql end declare section;
main() {
exec sql connect to tcp:postgresql://localhost:5432/rick
 as rick user rick using secretpassword;
exec sql BEGIN WORK;
exec sql DECLARE mycursor CURSOR FOR SELECT age, fname
   FROM children;
exec sql OPEN mycursor;
exec sql FETCH NEXT IN mycursor into
   :child_age:ind_child_age,
   :child_name:ind_child_name;
if (sqlca.sqlcode < 0)
printf("error code %d, message %s, rows %d, warning
 %c\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc,
   sqlca.sqlerrd[2], sqlca.sqlwarn[0]);
while (sqlca.sqlcode == 0) {
if (sqlca.sqlcode >= 0) {
if (ind_child_name >= 0) {
child_name.arr[sizeof(child_name.arr) -1] = '\0';
} else {
strcpy(child_name.arr, "Unknown");
}
if (ind_child_age >= 0) {
printf("Child's name and age %s, %d\n", child_name.arr,
    child_age);
} else {
printf("Child's name %s\n", child_name.arr);
}
}
exec sql FETCH NEXT IN mycursor into
   :child_age:ind_child_age,
   :child_name:ind_child_name;
if (sqlca.sqlcode < 0)
printf("error code %d, message %s, rows %d, warning
 %c\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc,
   sqlca.sqlerrd[2], sqlca.sqlwarn[0]);
} /* end of while loop */
exec sql CLOSE mycursor;
exec sql COMMIT WORK;
exec sql disconnect    




  << Page 4 of 4