Index: mod_nsd/postgres.c
===================================================================
RCS file: /usr/local/cvsroot/mod_nsd/postgres.c,v
diff -u -r1.1.1.1 -r1.4
--- mod_nsd/postgres.c	13 Apr 2001 21:09:32 -0000	1.1.1.1
+++ mod_nsd/postgres.c	17 Apr 2001 04:20:12 -0000	1.4
@@ -1,3 +1,4 @@
+
 /* $Header$ */
 /*
  * The contents of this file are subject to the AOLserver Public License
@@ -28,6 +29,7 @@
  * version of this file under either the License or the GPL.
  */
 
+/* NOTE: for ACS/pg use, you need to define FOR_ACS_USE! */
 
 #include "ns.h"
 /* If we're under AOLserver 3, we don't need some things.
@@ -112,6 +114,50 @@
    skeleton.  Will revisit this implementation for cleanup once functionality
    fully verified.  Lamar Owen <lamar.owen@wgcr.org> Feb 6, 2000. */
 
+/* Merge with AOLserver 3.0rc1's nspostgres.c -- more error checking from
+   Jan Wieck.
+
+   Also, changed behavior: if the datestyle parameter is not set in config
+   file, set it to be 'iso' by default -- it was not getting correctly set.
+
+   Wrapped ACS stuff inside FOR_ACS_USE #ifdef's.
+
+   3-21-2000 lamar.owen@wgcr.org */
+
+/* 2000-03-28: added check for the existence of the PGDATESTYLE envvar
+   and do no setting of datestyle if it exists.  lamar.owen@wgcr.org */
+
+/* 2000-03-28: take two: make datestyle parameter override envvar, and make 
+   the default go away. LRO */
+
+/* 2000-05-04: Added blob_select_file command.  Needed an inverse to
+   blob_dml_file to support porting of webmail. danw@rtp.ericsson.se*/
+
+/* 2000-12-30: Added bind variable emulation to support acs 4.0 porting.
+   dcwickstrom@earthlink.net*/
+
+/* 2001-03-??: Added automatic quoting of emulated bind variables in order
+   to make it compatible with the analogous routine in the Oracle driver.
+   dhogaza@pacifier.com*/
+
+/* 2001-04-14: Added Henry Minsky's patch which echoes changes in the
+   Oracle driver to stream blob data directly to the connection 
+   rather than first spool to a temp file.  Since the call to return
+   the file to the user doesn't return until after the operation is
+   complete, spooling was a waste of time and resource.
+   dhogaza@pacifier.com*/
+
+/* Contributors to this file include:
+
+	Don Baccus		<dhogaza@pacifier.com>
+	Lamar Owen		<lamar.owen@wgcr.org>
+	Jan Wieck		<wieck@debis.com>
+	Keith Pasket		(SDL/USU)
+	Scott Cannon, Jr.	(SDL/USU)
+        Dan Wickstrom           <danw@rtp.ericsson.se>
+
+	Original example driver by Jim Davidson */
+
 #define DRIVER_NAME             "PostgreSQL"
 #define OID_QUOTED_STRING       " oid = '"
 #define STRING_BUF_LEN          256
@@ -123,22 +169,37 @@
 #define TRUE 1
 #endif
 
-static void Ns_PgUnQuoteOidString(Ns_DString *sql);
+static void 	Ns_PgUnQuoteOidString(Ns_DString *sql);
 static char    *Ns_PgName(Ns_DbHandle *handle);
 static char    *Ns_PgDbType(Ns_DbHandle *handle);
 static int      Ns_PgOpenDb(Ns_DbHandle *dbhandle);
 static int      Ns_PgCloseDb(Ns_DbHandle *dbhandle);
 static int      Ns_PgGetRow(Ns_DbHandle *handle, Ns_Set *row);
 static int      Ns_PgFlush(Ns_DbHandle *handle);
 
-/* ns_column and ns_table reimplement for ACS */
-static Ns_DbTableInfo *Ns_PgGetTableInfo(Ns_DbHandle *handle, char *table);
-static char    *Ns_PgTableList(Ns_DString *pds, Ns_DbHandle *handle, int includesystem);
+/* Clunky construct follows :-) We want these statics for either AS 2.3
+   OR for ACS/pg under AS3 -- plain AS3 doesn't get these */
 
 /* Hack out the extended_table_info stuff if AOLserver 3, and add in our
    driver's reimplement of ns_table and ns_column */
 
 #ifdef NS_AOLSERVER_3_PLUS
+#ifdef FOR_ACS_USE
+
+/* So that we don't have to do this clunky thing again, set a define */
+#define NOT_AS3_PLAIN
+
+/* A linked list to use when parsing SQL. */
+
+typedef struct _string_list_elt {
+  char *string;
+  struct _string_list_elt *next;
+} string_list_elt_t;
+
+
+static Ns_DbTableInfo *Ns_PgGetTableInfo(Ns_DbHandle *handle, char *table);
+static char    *Ns_PgTableList(Ns_DString *pds, Ns_DbHandle *handle, int includesystem);
+
 static int pg_column_command (ClientData dummy, Tcl_Interp *interp, 
 			       int argc, char *argv[]);
 static int pg_table_command (ClientData dummy, Tcl_Interp *interp, 
@@ -149,19 +210,32 @@
 static void Ns_DbAddColumnInfo (Ns_DbTableInfo *tinfo, Ns_Set *column_info);
 static int Ns_DbColumnIndex (Ns_DbTableInfo *tinfo, char *name);
 
-#else
+#endif /* FOR_ACS_USE */
 
+/* PLAIN AS 3! */
+#define AS3_PLAIN  /* that is, AS3 without the ACS extensions */
+
+#else /* NS_AOLSERVER_3_PLUS */
+
+/* define NOT_AS3_PLAIN here as well, so that a single ifdef can be used */
+#define NOT_AS3_PLAIN
+
+static Ns_DbTableInfo *Ns_PgGetTableInfo(Ns_DbHandle *handle, char *table);
+static char    *Ns_PgTableList(Ns_DString *pds, Ns_DbHandle *handle, int includesystem);
+
 static char    *Ns_PgBestRowId(Ns_DString *pds, Ns_DbHandle *handle, char *table);
 
 #endif /* NS_AOLSERVER_3_PLUS */
 
-static int	 Ns_PgServerInit(char *hServer, char *hModule, char *hDriver);
-static char    *pgName = DRIVER_NAME;
-static unsigned int	pgCNum = 0;
+static int	Ns_PgServerInit(char *hServer, char *hModule, char *hDriver);
+static void	Ns_PgSetErrorstate(Ns_DbHandle *handle);
 static int      Ns_PgExec(Ns_DbHandle *handle, char *sql);
 static Ns_Set  *Ns_PgBindRow(Ns_DbHandle *handle);
 static int      Ns_PgResetHandle(Ns_DbHandle *handle);
 
+static char	     *pgName = DRIVER_NAME;
+static unsigned int   pgCNum = 0;
+
 /*-
  * 
  * The NULL-terminated PgProcs[] array of Ns_DbProc structures is the
@@ -209,14 +283,17 @@
     int             in_transaction;
 }               NsPgConn;
 
+#ifdef notdef
 DllExport int   Ns_ModuleVersion = 1;
+#endif
 
 static char datestyle[STRING_BUF_LEN];
 
 DllExport int
 Ns_DbDriverInit(char *hDriver, char *configPath)
 {
     char *t;
+    char *e;
 
     /* 
      * Register the PostgreSQL driver functions with nsdb.
@@ -230,6 +307,8 @@
     }
     Ns_Log(Notice, "%s loaded.", pgName);
 
+    e = getenv("PGDATESTYLE");
+
     t = Ns_ConfigGetValue(configPath, "DateStyle");
 
     strcpy(datestyle, "");
@@ -240,10 +319,17 @@
             strcpy(datestyle, "set datestyle to '");
             strcat(datestyle, t);
             strcat(datestyle, "'");
-        } else {
+            if (e) {
+                Ns_Log(Notice, "PGDATESTYLE overridden by datestyle param.");
+             }
+         } else {
             Ns_Log(Error, "Illegal value for datestyle - ignored");
-        }
-    }
+          }
+     } else {
+        if (e) {
+            Ns_Log(Notice, "PGDATESTYLE setting used for datestyle.");
+         }
+       }
     return NS_OK;
 
 }
@@ -299,7 +385,7 @@
         *db++ = '\0';
 	if (!strcmp(host, "localhost")) {
 	    Ns_Log(Notice, "Opening %s on %s", db, host);
-	    pgConn = PQsetdbLogin(NULL, NULL, NULL, NULL, db, handle->user,
+	    pgConn = PQsetdbLogin(NULL, port, NULL, NULL, db, handle->user,
                                   handle->password);
 	} else {
 	    Ns_Log(Notice, "Opening %s on %s, port %s", db, host, port);
@@ -323,7 +409,7 @@
             handle->connection = nsConn;
 
             if (strlen(datestyle)) { 
-                return Ns_PgExec(handle, "set datestyle to 'ISO'") == NS_DML ?
+                return Ns_PgExec(handle, datestyle) == NS_DML ?
                   NS_OK : NS_ERROR;
             }
             return NS_OK;
@@ -371,7 +457,37 @@
     return NS_OK;
 }
 
+static void
+Ns_PgSetErrorstate(Ns_DbHandle *handle)
+{
+    NsPgConn  *nsConn = handle->connection;
+    Ns_DString        *nsMsg  = &(handle->dsExceptionMsg);
 
+    Ns_DStringTrunc(nsMsg, 0);
+
+    switch (PQresultStatus(nsConn->res)) {
+        case PGRES_EMPTY_QUERY:
+        case PGRES_COMMAND_OK:
+        case PGRES_TUPLES_OK:
+        case PGRES_COPY_OUT:
+        case PGRES_COPY_IN:
+        case PGRES_NONFATAL_ERROR:
+              Ns_DStringAppend(nsMsg, PQresultErrorMessage(nsConn->res));
+              break;
+
+        case PGRES_FATAL_ERROR:
+              Ns_DStringAppend(nsMsg, PQresultErrorMessage(nsConn->res));
+              break;
+
+        case PGRES_BAD_RESPONSE:
+              Ns_DStringAppend(nsMsg, "PGRES_BAD_RESPONSE ");
+              Ns_DStringAppend(nsMsg, PQresultErrorMessage(nsConn->res));
+              break;
+    }
+}
+
+
+
 /* Set the current transaction state based on the query pointed to by
  * "sql".  Should be called only after the query has successfully been
  * executed.
@@ -445,6 +561,12 @@
 
     nsConn->res = PQexec(nsConn->conn, dsSql.string);
 
+    /* Set error result for exception message -- not sure that this
+       belongs here in DRB-improved driver..... but, here it is
+       anyway, as it can't really hurt anything :-) */
+   
+    Ns_PgSetErrorstate(handle);
+
     /* This loop should actually be safe enough, but we'll take no 
      * chances and guard against infinite retries with a counter.
      */
@@ -460,6 +582,8 @@
          */
 
         int retry_query = PQresultStatus(nsConn->res) == PGRES_NONFATAL_ERROR;
+        
+        /* Reconnect messages need to be logged regardless of Verbose. */    
 
         Ns_Log(Notice, "%s: Trying to reopen database connection", asfuncname);
 
@@ -570,8 +694,9 @@
     return NS_OK;
 }
 
+/* EXCISE for PLAIN AS3 */
+#ifdef NOT_AS3_PLAIN
 
-
 /*
  * Ns_PgSelect - Send a query which should return rows.
  * 
@@ -622,8 +747,10 @@
     return (row);
 }
 
+#endif /*NOT_AS3_PLAIN */
+
 /*
- * Ns_PgGetRow - Fetch rows after an Ns_PgSelect.
+ * Ns_PgGetRow - Fetch rows after an Ns_PgSelect or Ns_PgExec.
  */
 static int
 Ns_PgGetRow(Ns_DbHandle *handle, Ns_Set *row) {
@@ -691,7 +818,9 @@
     return NS_OK;
 }
 
+/* Not needed for a PLAIN AS3 driver */
 
+#ifdef NOT_AS3_PLAIN
 /*
  * Ns_DbTableInfo - Return system catalog information (columns, types, etc.)
  * about a table.
@@ -729,8 +858,8 @@
 
     if (row != NULL) {
         while ((status = Ns_PgGetRow(handle, row)) == NS_OK) {
-            name = Ns_SetValue (row, 0);
-            type = Ns_SetValue (row, 1);
+            name = row->fields[0].value;
+            type = row->fields[1].value;
             if (name == NULL || type == NULL) {
                 Ns_Log(Error, "Ns_PgGetTableInfo(%s):  Invalid 'pg_attribute' entry for table:  %s",
                        handle->datasource, table);
@@ -740,10 +869,13 @@
             /*
              * NB:  Move the fields directly from the row
              * Ns_Set to the col Ns_Set to avoid a data copy.
-	     * We don't bother...
              */
-            col = Ns_SetCreate(name);
-            Ns_SetPut(col, "type", type);
+            col = Ns_SetCreate(NULL);
+            col->name = name;
+            Ns_SetPut(col, "type", NULL);
+            col->fields[0].value = type;
+            row->fields[0].value = NULL;
+            row->fields[1].value = NULL;
             if (tinfo == NULL) {
                 tinfo = Ns_DbNewTableInfo(table);
             }
@@ -792,7 +924,7 @@
 
     if (row != NULL) {
         while ((status = Ns_DbGetRow(handle, row)) == NS_OK) {
-            table = Ns_SetValue (row, 0);
+            table = row->fields[0].value;
             if (table == NULL) {
                 Ns_Log(Warning, "Ns_PgTableList(%s):  NULL relname in 'pg_class' table.",
                        handle->datasource);
@@ -808,6 +940,8 @@
     return (NULL);
 }
 
+#endif /*NOT_AS3_PLAIN */
+
 /* Excise for AOLserver 3*/
 #ifndef NS_AOLSERVER_3_PLUS
 /*
@@ -830,8 +964,9 @@
 
 /* end of extended table info stuff */
 #endif /* NS_AOLSERVER_3_PLUS */
-#include <stdio.h>
 
+/* ACS BLOBing stuff */
+#ifdef FOR_ACS_USE
 
 /*
  * This is a slight modification of the encoding scheme used by
@@ -900,50 +1035,19 @@
 		*buf++ = DEC(c3) << 6 | DEC(c4);
 }
 
-/* ns_pg blob_write db blob_id
- * Write a pseudo-blob to the current connection.  Some of this
- * shamelessly lifted from ora8.c
+/* ns_pg blob_get db blob_id
+ * returns the value of the blob to the Tcl caller.
  */
 
 static int
-blob_write(Tcl_Interp *interp, Ns_DbHandle *handle, char* lob_id)
+blob_get(Tcl_Interp *interp, Ns_DbHandle *handle, char* lob_id)
 {
     NsPgConn	*nsConn = (NsPgConn *) handle->connection;
-	char		*filename;
-	int			result;
-	Ns_Conn		*conn;
 	int			segment;
 	char		query[100];
-	int			fd;
 	char		*segment_pos;
 	int			nbytes = 0;
 
-	/* spool to a tmp file, then return that out the conn
-	 * note that tmpnam returns malloc'd memory.
-	 */
-
-	filename = tempnam (NULL, "tadgp");
-
-	if (filename == NULL) 
-	{
-		Tcl_AppendResult (interp, "could not create temporary file to spool "
-							"BLOB/CLOB result", NULL);
-		return TCL_ERROR;
-	}
-
-
-	fd = open (filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
-
-	if (fd < 0) 
-	{
-		Ns_Log (Error, "Can't open %s for writing. error %d(%s)",
-				filename, errno, strerror(errno));
-		Tcl_AppendResult (interp, "can't open file ", filename,
-				 " for writing. ",
-			"received error ", strerror(errno), NULL);
-		return TCL_ERROR;
-	}
-
 	segment = 1;
 
 	strcpy(query, "SELECT BYTE_LEN, DATA FROM LOB_DATA WHERE LOB_ID = ");
@@ -956,7 +1060,7 @@
 		char	*data_column;
 		char	*byte_len_column;
 		int		i, j, n, byte_len;
-		char	buf[6000];
+		char	buf[6001];
 
 		sprintf(segment_pos, "%d", segment);
 		if (Ns_PgExec(handle, query) != NS_ROWS) {
@@ -974,49 +1078,87 @@
 		for (i=0, j=0; n > 0; i += 4, j += 3, n -= 3) {
 			decode3(&data_column[i], &buf[j], n);
 		}
-		write(fd, buf, byte_len);
+        buf[byte_len] = '\0';
+		Tcl_AppendResult(interp, buf, NULL);
 		segment++;
     }
 
-	close(fd);
-
 	PQclear(nsConn->res);
 	nsConn->res = NULL;
 
-	conn = Ns_TclGetConn (interp);
+	return TCL_OK;
+}
 
-	if (conn == NULL) 
-	{
-		Tcl_AppendResult (interp, "no connection", NULL);
-		free (filename);
-		return TCL_ERROR;
-	}
 
-	fd = open (filename, O_RDONLY);
+/** 
+ * Write the contents of BUFP to a file descriptor or to
+ * the network connection directly.
+ *
+ * Lifted from Oracle driver.
+ */
+static int
+stream_actually_write (int fd, Ns_Conn *conn, void *bufp, int length, int to_conn_p)
+{
+  int bytes_written = 0;
 
-	if (fd == -1) 
-	{
-		Ns_Log (Error, "Error opening file %s: %d(%s)",
-				filename, errno, strerror(errno));
-		Tcl_AppendResult (interp, "can't open file ", filename,	
-				 " for reading. ", "received error ", strerror(errno), NULL);
-		return TCL_ERROR;
-	}
-	  
-	result = Ns_ConnSendFd (conn, fd, nbytes);
+  if (to_conn_p)
+    {
+      if (Ns_WriteConn (conn, bufp, length) == NS_OK) 
+        {
+	  bytes_written = length;
+        } 
+      else 
+        {
+	  bytes_written = 0;
+        }
+    }
+  else
+    {
+      bytes_written = write (fd, bufp, length);
+    }
 
-	close (fd);
+  return bytes_written;
+  
+} /* stream_actually_write */
 
-	if (result != NS_OK) 
-	{
-		Tcl_AppendResult (interp, "could not return file ", filename, NULL);
-		free (filename);
-		return TCL_ERROR;
-	}
 
-	unlink (filename);
-	free (filename);
+/* ns_pg blob_put blob_id value
+ * Stuff the contents of value into the pseudo-blob blob_id
+ */
 
+static int
+blob_put(Tcl_Interp *interp, Ns_DbHandle *handle, char* blob_id,
+			char* value)
+{
+	int			i, j, segment, value_len, segment_len;
+	char		out_buf[8001], query[100];
+	char		*segment_pos, *value_ptr;
+
+    value_len = strlen(value);
+    value_ptr = value;
+
+	strcpy(query, "INSERT INTO LOB_DATA VALUES(");
+	strcat(query, blob_id);
+	strcat(query, ",");
+	segment_pos = query + strlen(query);
+	segment = 1;
+
+	while (value_len > 0) {
+	    segment_len = value_len > 6000 ? 6000 : value_len;
+        value_len -= segment_len;
+		for (i = 0, j = 0; i < segment_len; i += 3, j+=4) {
+			encode3(&value_ptr[i], &out_buf[j]);
+		}
+		out_buf[j] = '\0';
+		sprintf(segment_pos, "%d, %d, '%s')", segment, segment_len, out_buf);
+		if (Ns_PgExec(handle, query) != NS_DML) {
+			Tcl_AppendResult(interp, "Error inserting data into BLOB", NULL);
+			return TCL_ERROR;
+		}
+        value_ptr += segment_len;
+		segment++;
+	}
+    Ns_Log(Notice, "done");
 	return TCL_OK;
 }
 
@@ -1029,7 +1171,7 @@
 			char* filename)
 {
 	int			fd, i, j, segment, readlen;
-	char		in_buf[6000], out_buf[8000], query[100];
+	char		in_buf[6000], out_buf[8001], query[100];
 	char		*segment_pos;
 
 	fd = open (filename, O_RDONLY);
@@ -1067,7 +1209,548 @@
 	return TCL_OK;
 }
 
+/* ns_pg blob_select_file db blob_id filename
+ * Write a pseudo-blob to the passed in temp file name.  Some of this
+ * shamelessly lifted from ora8.c. 
+ * DanW - This is just blob_write, except it doesn't send anything out the
+ *        connection.
+ * .
+ * Combined blob_select_file and blob_write:
+ * If you want to write to the network connection, set TO_CONN_P to TRUE
+ * and pass a null filename.
+ *
+ * If you want to write the blob to a file, set TO_CONN_P = FALSE, and
+ * pass the filename in.
+ */
+
+static int
+blob_send_to_stream(Tcl_Interp *interp, Ns_DbHandle *handle, char* lob_id, 
+		    int to_conn_p, char* filename)
+{
+  NsPgConn	*nsConn = (NsPgConn *) handle->connection;
+  int		segment;
+  char		query[100];
+  int		fd;
+  char		*segment_pos;
+  Ns_Conn       *conn;
+
+  if (to_conn_p) 
+    {  
+      conn = Ns_TclGetConn(interp);
+      
+      /* this Shouldn't Happen, but spew an error just in case */
+      if (conn == NULL) 
+        {
+	  Ns_Log (Error, "blob_send_to_stream: No AOLserver conn available");
+
+	  Tcl_AppendResult (interp, "No AOLserver conn available", NULL);
+	  goto bailout;
+        }
+    } else {
+      if (filename == NULL) 
+	{
+	  Tcl_AppendResult (interp, "could not create temporary file to spool "
+			    "BLOB/CLOB result", NULL);
+	  return TCL_ERROR;
+	}
+
+
+      fd = open (filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
+
+      if (fd < 0) 
+	{
+	  Ns_Log (Error, "Can't open %s for writing. error %d(%s)",
+		  filename, errno, strerror(errno));
+	  Tcl_AppendResult (interp, "can't open file ", filename,
+			    " for writing. ",
+			    "received error ", strerror(errno), NULL);
+	  return TCL_ERROR;
+	}
+    }
+
+  segment = 1;
+
+  strcpy(query, "SELECT BYTE_LEN, DATA FROM LOB_DATA WHERE LOB_ID = ");
+  strcat(query, lob_id);
+  strcat(query, " AND SEGMENT = ");
+
+  segment_pos = query + strlen(query);
+
+  for (;;) {
+    char	*data_column;
+    char	*byte_len_column;
+    int		i, j, n, byte_len;
+    char	buf[6000];
+
+    sprintf(segment_pos, "%d", segment);
+    if (Ns_PgExec(handle, query) != NS_ROWS) {
+      Tcl_AppendResult(interp, "Error selecting data from BLOB", NULL);
+      return TCL_ERROR;
+    }
+
+    if (PQntuples(nsConn->res) == 0) break;
+
+    byte_len_column = PQgetvalue(nsConn->res, 0, 0);
+    data_column = PQgetvalue(nsConn->res, 0, 1);
+    sscanf(byte_len_column, "%d", &byte_len);
+    n = byte_len;
+    for (i=0, j=0; n > 0; i += 4, j += 3, n -= 3) {
+      decode3(&data_column[i], &buf[j], n);
+    }
+
+    stream_actually_write (fd, conn, buf, byte_len, to_conn_p);
+    segment++;
+  }
+
+ bailout:
+  if (!to_conn_p)
+    {
+      close (fd);
+    }
+
+  PQclear(nsConn->res);
+  nsConn->res = NULL;
+
+  return TCL_OK;
+}
+
+
+#endif /* FOR_ACS_USE */
+
+#ifdef FOR_ACS_USE
+
 /*
+ *----------------------------------------------------------------------
+ * BadArgs --
+ *
+ *      Common routine that creates bad arguments message.
+ *
+ * Results:
+ *      Return TCL_ERROR and set bad argument message as Tcl result.
+ *
+ * Side effects:
+ *      None.
+ *
+ *----------------------------------------------------------------------
+ */
+
+static int
+BadArgs(Tcl_Interp *interp, char **argv, char *args)
+{
+    Tcl_AppendResult(interp, "wrong # args: should be \"",
+        argv[0], " ", argv[1], NULL);
+    if (args != NULL) {
+        Tcl_AppendResult(interp, " ", args, NULL);
+    }
+    Tcl_AppendResult(interp, "\"", NULL);
+
+    return TCL_ERROR;
+}
+
+
+/*
+ *----------------------------------------------------------------------
+ * DbFail --
+ *
+ *      Common routine that creates database failure message.
+ *
+ * Results:
+ *      Return TCL_ERROR and set database failure message as Tcl result.
+ *
+ * Side effects:
+ *      None.
+ *
+ *----------------------------------------------------------------------
+ */
+
+static int
+DbFail(Tcl_Interp *interp, Ns_DbHandle *handle, char *cmd, char* sql)
+{
+  Ns_Free(sql);
+  Tcl_AppendResult(interp, "Database operation \"", cmd, "\" failed", NULL);
+  if (handle->cExceptionCode[0] != '\0') {
+    Tcl_AppendResult(interp, " (exception ", handle->cExceptionCode,
+                     NULL);
+    if (handle->dsExceptionMsg.length > 0) {
+      Tcl_AppendResult(interp, ", \"", handle->dsExceptionMsg.string,
+                       "\"", NULL);
+    }
+    Tcl_AppendResult(interp, ")", NULL);
+  }
+
+  return TCL_ERROR;
+}
+
+/* 
+ * utility functions for dealing with string lists 
+ */
+
+static string_list_elt_t *
+string_list_elt_new(char *string)
+{
+  string_list_elt_t *elt = 
+    (string_list_elt_t *) Ns_Malloc(sizeof(string_list_elt_t));
+  elt->string = string;
+  elt->next = 0;
+
+  return elt;
+
+} /* string_list_elt_new */
+
+
+
+static int 
+string_list_len (string_list_elt_t *head)
+{
+  int i = 0;
+
+  while (head != NULL) {
+    i++;
+    head = head->next;
+  }
+
+  return i; 
+
+} /* string_list_len */
+
+
+
+/* Free the whole list and the strings in it. */
+
+static void 
+string_list_free_list (string_list_elt_t *head)
+{
+  string_list_elt_t *elt;
+
+  while (head) {
+    Ns_Free(head->string);
+    elt = head->next;
+    Ns_Free(head);
+    head = elt;
+  }
+
+} /* string_list_free_list */
+
+
+
+/* Parse a SQL string and return a list of all
+ * the bind variables found in it.
+ */
+
+static void
+parse_bind_variables(char *input, 
+                     string_list_elt_t **bind_variables, 
+                     string_list_elt_t **fragments)
+{
+  char *p, lastchar;
+  enum { base, instr, bind } state;
+  char *bindbuf, *bp;
+  char *fragbuf, *fp;
+  string_list_elt_t *elt, *head=0, *tail=0;
+  string_list_elt_t *felt, *fhead=0, *ftail=0;
+  int current_string_length = 0;
+  int first_bind = 0;
+
+  fragbuf = (char*)Ns_Malloc((strlen(input)+1)*sizeof(char));
+  fp = fragbuf;
+  bindbuf = (char*)Ns_Malloc((strlen(input)+1)*sizeof(char));
+  bp = bindbuf;
+
+  for (p = input, state=base, lastchar='\0'; *p != '\0'; lastchar = *p, p++) {
+
+    switch (state) {
+    case base:
+      if (*p == '\'') {
+	state = instr;
+        current_string_length = 0;
+        *fp++ = *p;
+      } else if ((*p == ':') && (*(p + 1) != ':') && (lastchar != ':')) {
+	bp = bindbuf;
+	state = bind;
+        *fp = '\0';
+        felt = string_list_elt_new(Ns_StrDup(fragbuf));
+        if(ftail == 0) {
+          fhead = ftail = felt;
+        } else {
+          ftail->next = felt;
+          ftail = felt;
+        }
+      } else {
+        *fp++ = *p;
+      }
+      break;
+
+    case instr:
+      if (*p == '\'' && (lastchar != '\'' || current_string_length == 0)) {
+	state = base;
+      }
+      current_string_length++;
+      *fp++ = *p;
+      break;
+
+    case bind:
+      if (*p == '=') {
+        state = base;
+        bp = bindbuf;
+        fp = fragbuf;
+      } else if (!(*p == '_' || *p == '$' || *p == '#' || isalnum((int)*p))) {
+	*bp = '\0';
+	elt = string_list_elt_new(Ns_StrDup(bindbuf));
+	if (tail == 0) {
+	  head = tail = elt;
+	} else {
+	  tail->next = elt;
+	  tail = elt;
+	}
+	state = base;
+        fp = fragbuf;
+	p--;
+      } else {
+	*bp++ = *p;
+      }
+      break;
+    }
+  }
+
+  if (state == bind) {
+    *bp = '\0';
+    elt = string_list_elt_new(Ns_StrDup(bindbuf));
+    if (tail == 0) {
+      head = tail = elt;
+    } else {
+      tail->next = elt;
+      tail = elt;
+    }
+  } else {
+    *fp = '\0';
+    felt = string_list_elt_new(Ns_StrDup(fragbuf));
+    if (ftail == 0) {
+      fhead = ftail = felt;
+    } else {
+      ftail->next = felt;
+      ftail = felt;
+    }
+  }
+  
+  Ns_Free(fragbuf);
+  Ns_Free(bindbuf);
+  *bind_variables = head;
+  *fragments      = fhead;  
+
+  return;
+
+} /* parse_bind_variables */
+
+
+/*
+ * PgBindCmd - This function implements the "ns_pg_bind" Tcl command 
+ * installed into each interpreter of each virtual server.  It provides 
+ * for the parsing and substitution of bind variables into the original 
+ * sql query.  This is an emulation only. Postgresql doesn't currently 
+ * support true bind variables yet.
+ */
+
+static int
+PgBindCmd(ClientData dummy, Tcl_Interp *interp, int argc, char **argv) {
+
+  string_list_elt_t *bind_variables;
+  string_list_elt_t *var_p;
+  string_list_elt_t *sql_fragments;
+  string_list_elt_t *frag_p;
+  Ns_DString         ds;
+  Ns_DbHandle       *handle;
+  Ns_Set            *rowPtr;
+  Ns_Set            *set   = NULL; 
+  char              *cmd;
+  char              *sql;
+  char              *value = NULL;
+  char              *p;
+  int               value_frag_len = 0;
+
+  if (argc < 4 || (!STREQ("-bind", argv[3]) && (argc != 4)) || 
+       (STREQ("-bind", argv[3]) && (argc != 6))) {
+    return BadArgs(interp, argv, "dbId sql");
+  }
+
+  if (Ns_TclDbGetHandle(interp, argv[2], &handle) != TCL_OK) {
+    return TCL_ERROR;
+  }
+
+  Ns_DStringFree(&handle->dsExceptionMsg);
+  handle->cExceptionCode[0] = '\0';
+
+  /*
+   * Make sure this is a PostgreSQL handle before accessing
+   * handle->connection as an NsPgConn.
+   */
+
+  if (Ns_DbDriverName(handle) != pgName) {
+    Tcl_AppendResult(interp, "handle \"", argv[1], "\" is not of type \"",
+                     pgName, "\"", NULL);
+    return TCL_ERROR;
+  }
+
+  cmd = argv[1];
+
+  if (STREQ("-bind", argv[3])) {
+    set = Ns_TclGetSet(interp, argv[4]);
+    if (set == NULL) {
+      Tcl_AppendResult (interp, "invalid set id `", argv[4], "'", NULL);
+      return TCL_ERROR;	      
+    }
+    sql = Ns_StrDup(argv[5]);
+  } else {
+    sql = Ns_StrDup(argv[3]);
+  }
+
+  Ns_Log(Debug,"PgBindCmd: sql = %s", sql);
+
+  /*
+   * Parse the query string and find the bind variables.  Return
+   * the sql fragments so that the query can be rebuilt with the 
+   * bind variable values interpolated into the original query.
+   */
+
+  parse_bind_variables(sql, &bind_variables, &sql_fragments);  
+
+  if (string_list_len(bind_variables) > 0) {
+
+    Ns_DStringInit(&ds);
+
+    /*
+     * Rebuild the query and substitute the actual tcl variable values
+     * for the bind variables.
+     */
+
+    for (var_p = bind_variables, frag_p = sql_fragments; 
+         var_p != NULL || frag_p != NULL;) {
+    
+      if (frag_p != NULL) {
+        Ns_DStringAppend(&ds, frag_p->string);
+        frag_p = frag_p->next;
+      }
+   
+      if (var_p != NULL) {
+        if (set == NULL) {
+          value = Tcl_GetVar(interp, var_p->string, 0);
+        } else {
+          value = Ns_SetGet(set, var_p->string);
+        }
+        if (value == NULL) {
+          Tcl_AppendResult (interp, "undefined variable `", var_p->string,
+                            "'", NULL);
+          Ns_DStringFree(&ds);
+          string_list_free_list(bind_variables);
+          string_list_free_list(sql_fragments);
+          Ns_Free(sql);
+          return TCL_ERROR;
+        }
+        Ns_Log(Debug,"PgBindCmd: bind var: %s = %s", var_p->string, value);
+
+        if ( strlen(value) == 0 ) {
+            /*
+             * DRB: If the Tcl variable contains the empty string, pass a NULL
+             * as the value.
+             */
+            Ns_DStringAppend(&ds, "NULL");
+        } else {
+            /*
+             * DRB: We really only need to quote strings, but there is one benefit
+             * to quoting numeric values as well.  A value like '35 union select...'
+             * substituted for a legitimate value in a URL to "smuggle" SQL into a
+             * script will cause a string-to-integer conversion error within Postgres.
+             * This conversion is done before optimization of the query, so indices are
+             * still used when appropriate.
+             */
+            Ns_DStringAppend(&ds, "'");       
+
+            /*
+             * DRB: Unfortunately, we need to double-quote quotes as well...
+             */ 
+            for (p = value; *p; p++) {
+                if (*p == '\'') {
+                    if (p > value) {
+                        Ns_DStringNAppend(&ds, value, p-value);
+                    }
+                    value = p;
+                    Ns_DStringAppend(&ds, "'");
+                }
+            }
+
+            if (p > value) {
+                Ns_DStringAppend(&ds, value);
+            }
+
+            Ns_DStringAppend(&ds, "'");       
+        }
+        var_p = var_p->next;
+      }
+    }
+  
+    Ns_Free(sql);
+    sql = Ns_DStringExport(&ds);
+    Ns_DStringFree(&ds);
+    Ns_Log(Debug, "PgBindCmd: query with bind variables substituted = %s",sql);
+  }
+
+  string_list_free_list(bind_variables);
+  string_list_free_list(sql_fragments);
+
+  if (STREQ(cmd, "dml")) {
+    if (Ns_DbDML(handle, sql) != NS_OK) {
+      return DbFail(interp, handle, cmd, sql);
+    }
+  } else if (STREQ(cmd, "1row")) {
+    rowPtr = Ns_Db1Row(handle, sql);
+    if (rowPtr == NULL) {
+      return DbFail(interp, handle, cmd, sql);
+    }
+    Ns_TclEnterSet(interp, rowPtr, 1);
+
+  } else if (STREQ(cmd, "0or1row")) {
+    int nrows;
+
+    rowPtr = Ns_Db0or1Row(handle, sql, &nrows);
+    if (rowPtr == NULL) {
+      return DbFail(interp, handle, cmd, sql);
+    }
+    if (nrows == 0) {
+      Ns_SetFree(rowPtr);
+    } else {
+      Ns_TclEnterSet(interp, rowPtr, 1);
+    }
+
+  } else if (STREQ(cmd, "select")) {
+    rowPtr = Ns_DbSelect(handle, sql);
+    if (rowPtr == NULL) {
+      return DbFail(interp, handle, cmd, sql);
+    }
+    Ns_TclEnterSet(interp, rowPtr, 0);
+
+  } else if (STREQ(cmd, "exec")) {
+    switch (Ns_DbExec(handle, sql)) {
+    case NS_DML:
+      Tcl_SetResult(interp, "NS_DML", TCL_STATIC);
+      break;
+    case NS_ROWS:
+      Tcl_SetResult(interp, "NS_ROWS", TCL_STATIC);
+      break;
+    default:
+      return DbFail(interp, handle, cmd, sql);
+      break;
+    }
+
+  } else {
+    return DbFail(interp, handle, cmd, sql);    
+  } 
+  Ns_Free(sql);
+
+  return TCL_OK;
+}
+
+#endif /* FOR_ACS_USE */
+
+/*
  * PgCmd - This function implements the "ns_pg" Tcl command installed into
  * each interpreter of each virtual server.  It provides access to features
  * specific to the PostgreSQL driver.
@@ -1094,17 +1777,44 @@
         return TCL_ERROR;
     }
 
+/* BLOBing functions only if FOR_ACS_USE */
+#ifdef FOR_ACS_USE
+
     if (!strcmp(argv[1], "blob_write")) {
         if (argc != 4) {
         	Tcl_AppendResult(interp, "wrong # args: should be \"",
                          	argv[0], " command dbId blobId\"", NULL);
         	return TCL_ERROR;
 		}
-		return blob_write(interp, handle, argv[3]);
+		return blob_send_to_stream(interp, handle, argv[3], TRUE, NULL);
+	} else if (!strcmp(argv[1], "blob_get")) {
+    	if (argc != 4) {
+        	Tcl_AppendResult(interp, "wrong # args: should be \"",
+                         	argv[0], " command dbId blobId\"", NULL);
+        	return TCL_ERROR;
+    	}
+		if (!pgconn->in_transaction) {
+        	Tcl_AppendResult(interp,
+							 "blob_get only allowed in transaction", NULL);
+        	return TCL_ERROR;
+		}
+		return blob_get(interp, handle, argv[3]);
+	} else if (!strcmp(argv[1], "blob_put")) {
+    	if (argc != 5) {
+        	Tcl_AppendResult(interp, "wrong # args: should be \"",
+                         	argv[0], " command dbId blobId value\"", NULL);
+        	return TCL_ERROR;
+    	}
+		if (!pgconn->in_transaction) {
+        	Tcl_AppendResult(interp,
+							 "blob_put only allowed in transaction", NULL);
+        	return TCL_ERROR;
+		}
+		return blob_put(interp, handle, argv[3], argv[4]);
 	} else if (!strcmp(argv[1], "blob_dml_file")) {
     	if (argc != 5) {
         	Tcl_AppendResult(interp, "wrong # args: should be \"",
-                         	argv[0], " command dbId\"", NULL);
+                         	argv[0], " command dbId blobId filename\"", NULL);
         	return TCL_ERROR;
     	}
 		if (!pgconn->in_transaction) {
@@ -1113,8 +1823,17 @@
         	return TCL_ERROR;
 		}
 		return blob_dml_file(interp, handle, argv[3], argv[4]);
+        } else if (!strcmp(argv[1], "blob_select_file")) {
+        if (argc != 5) {
+        	Tcl_AppendResult(interp, "wrong # args: should be \"",
+                         	argv[0], " command dbId blobId filename\"", NULL);          
+        	return TCL_ERROR;
+        }
+                return blob_send_to_stream(interp, handle, argv[3], FALSE, argv[4]);
     }
 
+#endif /* FOR_ACS_USE */
+
     if (argc != 3) {
         Tcl_AppendResult(interp, "wrong # args: should be \"",
                          argv[0], " command dbId\"", NULL);
@@ -1146,7 +1865,11 @@
 	Tcl_SetResult(interp, string, TCL_VOLATILE);
     } else {
         Tcl_AppendResult(interp, "unknown command \"", argv[2],
-                         "\": should be db, host, options, port, error or status.", NULL);
+                         "\": should be db, host, options, port, error, ntuples, ",
+#ifdef FOR_ACS_USE
+                         "blob_write, blob_dml_file, blob_select_file, blob_put, ",
+#endif 
+                         "or status.", NULL);
         return TCL_ERROR;
     }
     return TCL_OK;
@@ -1159,10 +1882,17 @@
 static int
 Ns_PgInterpInit(Tcl_Interp *interp, void *ignored) {
     Tcl_CreateCommand(interp, "ns_pg", PgCmd, NULL, NULL);
+#ifdef FOR_ACS_USE
+    Tcl_CreateCommand(interp, "ns_pg_bind", PgBindCmd, NULL, NULL);
+#endif
 #ifdef NS_AOLSERVER_3_PLUS
+#ifdef FOR_ACS_USE
     Tcl_CreateCommand (interp, "ns_column", pg_column_command, NULL, NULL);
     Tcl_CreateCommand (interp, "ns_table", pg_table_command, NULL, NULL);
-#endif
+
+#endif /* FOR_ACS_USE */
+#endif /* NS_AOLSERVER_3_PLUS */
+
     return NS_OK;
 }
 
@@ -1249,9 +1979,11 @@
     }
 }
 
-/* Reimplement some things dropped from AOLserver 3 */
-#if defined(NS_AOLSERVER_3_PLUS)
+/* Reimplement some things dropped from AOLserver 3 for the ACS */
 
+#ifdef NS_AOLSERVER_3_PLUS
+#ifdef FOR_ACS_USE
+
 /* the AOLserver3 team removed some commands that are pretty vital
  * to the normal operation of the ACS (ArsDigita Community System).
  * We include definitions for them here
@@ -1658,5 +2390,5 @@
 } /* Ns_DbColumnIndex */
 
 
-
+#endif /* FOR_ACS_USE */
 #endif /* NS_AOLSERVER_3_PLUS */