On Fri, Dec 14, 2018 at 11:11:03AM -0600, Justin Pryzby wrote:
> Find attached patch with proof of concept for minimal implementation of
> prepared statements.

> And for PQexecPrepared, I added a conditional in query(), since that entire
> function appears to be exactly what's needed for the prepared case, too.
> Here, the hack is that I determine wether to call exec or execPrepared by
> running strstr(sql, "prep").  I'll solicit suggestions for a better way to do
> that now.

I addressed this by refactoring connQuery in the most blindingly simple way.

> I hacked query_formatted to accept an prepare=False.
> The difficulty is in replacing %s with $1, which needs to either have a list 
> of
> parameters over which to iterate, or at least an integer determining the 
> number
> of dollar parameters in literals.  Since PREPARE doesn't send parameters
> (that's quite the point), I passed a range() list as a hack to show that it
> could work.

Any suggestion how to address this ?

The interface currently takes a list which is only used for its length.

It'd be fine to add a different interface for query_prepared, taking just an
int, but that's just as ugly.

All I can think to do is to subclass tuple() and override getitem() and save
the max element which is accessed.  It's gross and it's probably for the best
that I couldn't make it work.

So an even grosser idea is to pass tuples of length varying by factors of 2
doing a binary search through except TypeError as e:

untested because I'm sure someone can find something less bad:

tuplen=1
tup=tuple('' for a in range(tuplen))
try:
        s%tup
except TypeError as e
        if 'not enough arguments' in e.args then
                tuplen*=2
        elif 'not all arguments converted' in e.args then
                tuplen=3*tuplen//4
        else:
                raise
return tuplen

Finally, do you think it's reasonable to consider releasing an update with
prepared statements in pg but not pgdb ?  I don't see a clear path for pgdb, as
I wrote earlier.

[pryzbyj@telsasoft PyGreSQL]$ PYTHONPATH=build/lib.linux-x86_64-2.7 python2.7 
-c "import pg; d=pg.DB('ts'); x=d.query_prepared(\"PREPARE x AS SELECT 
generate_series(1,%s)\", [1]); print d.execute_prepared('x',3).getresult()"
[(1,), (2,), (3,)]

Justin
diff --git a/pg.py b/pg.py
index e830a66..d480931 100644
--- a/pg.py
+++ b/pg.py
@@ -678,14 +678,23 @@ class Adapter:
         params.adapt = self.adapt
         return params
 
-    def format_query(self, command, values=None, types=None, inline=False):
+    def format_query(self, command, values=None, types=None, inline=False, prepare=False):
         """Format a database query using the given values and types."""
-        if not values:
+        if not values and not prepare:
             return command, []
         if inline and types:
             raise ValueError('Typed parameters must be sent separately')
         params = self.parameter_list()
-        if isinstance(values, (list, tuple)):
+        if prepare:
+            # In this case, "inline" doesn't make any sense ?
+            literals = []
+            for _ in values:
+                literals.append(params.add(None))
+            params=[] # Do not send params during PREPARE
+            # Or just do this:
+            literals = ['$%s'%x for x in range(1,1+len(values))]
+            command %= tuple(literals)
+        elif isinstance(values, (list, tuple)):
             if inline:
                 adapt = self.adapt_inline
                 literals = [adapt(value) for value in values]
@@ -1853,6 +1862,13 @@ class DB:
         self._do_debug(command)
         return self.db.query(command)
 
+    def execute_prepared(self, command, *args):
+        # Wraps shared library function for debugging.
+        if not self.db:
+            raise _int_error('Connection is not valid')
+        self._do_debug(command, args)
+        return self.db.execute_prepared(command, args)
+
     def query_formatted(self, command,
             parameters=None, types=None, inline=False):
         """Execute a formatted SQL command string.
@@ -1869,6 +1885,22 @@ class DB:
         return self.query(*self.adapter.format_query(
             command, parameters, types, inline))
 
+    def query_prepared(self, command, parameters=None):
+        """Prepare a statement for execution
+
+	command is a statement of the form "PREPARE name AS ..."
+	which may include parameters using Python format placeholders like as
+	query_formatted.
+
+	The statment can be later executed in two ways:
+	1. Using SQL calls: query("EXECUTE name [(parameter [, ...])]", inline=True)
+	2. Using library call to PQexecPrepared:
+
+        """
+        # XXX: check if no parameters sent ?
+        return self.query(*self.adapter.format_query(
+            command, parameters, prepare=True))
+
     def pkey(self, table, composite=False, flush=False):
         """Get or set the primary key of a table.
 
diff --git a/pgmodule.c b/pgmodule.c
index 08ed188..2781b49 100644
--- a/pgmodule.c
+++ b/pgmodule.c
@@ -2140,14 +2140,9 @@ connSource(connObject *self, PyObject *noargs)
 	return (PyObject *) npgobj;
 }
 
-/* database query */
-static char connQuery__doc__[] =
-"query(sql, [arg]) -- create a new query object for this connection\n\n"
-"You must pass the SQL (string) request and you can optionally pass\n"
-"a tuple with positional parameters.\n";
-
+/* Execute a query or previously prepared statement */
 static PyObject *
-connQuery(connObject *self, PyObject *args)
+ connQueryOrPrepared(connObject *self, PyObject *args, int is_prepared)
 {
 	PyObject	*query_obj;
 	PyObject	*param_obj = NULL;
@@ -2219,6 +2214,8 @@ connQuery(connObject *self, PyObject *args)
 		}
 	}
 
+	/* TODO: fail if prepared && !param_obj */
+
 	/* gets result */
 	if (nparms)
 	{
@@ -2287,8 +2284,17 @@ connQuery(connObject *self, PyObject *args)
 		}
 
 		Py_BEGIN_ALLOW_THREADS
-		result = PQexecParams(self->cnx, query, nparms,
-			NULL, parms, NULL, NULL, 0);
+		if (is_prepared) {
+			// In this case, the "query" is just the name of the
+			// prepared statement to execute.
+			// Note, no argument is passed for paramTypes.
+			result = PQexecPrepared(self->cnx, query, nparms,
+				parms, NULL, NULL, 0);
+		} else {
+			result = PQexecParams(self->cnx, query, nparms,
+				NULL/*paramTypes*/, parms, NULL, NULL, 0);
+		}
+
 		Py_END_ALLOW_THREADS
 
 		PyMem_Free((void *)parms);
@@ -2376,6 +2382,29 @@ connQuery(connObject *self, PyObject *args)
 	return (PyObject *) npgobj;
 }
 
+/* database query */
+static char connQuery__doc__[] =
+"query(sql, [arg]) -- create a new query object for this connection\n\n"
+"You must pass the SQL (string) request and you can optionally pass\n"
+"a tuple with positional parameters.\n";
+static PyObject *
+connQuery(connObject *self, PyObject *args)
+{
+	return connQueryOrPrepared(self, args, 0);
+}
+
+/* execute prepared statement */
+static char connExecPrepared__doc__[] =
+"query(sql, arg) -- create a new query object for this connection\n\n"
+"You must pass the name (string) of previously-prepared statement request and\n"
+"a tuple of parameters.\n";
+// XXX: should allow an empty typle or no params?
+static PyObject *
+connExecPrepared(connObject *self, PyObject *args)
+{
+	return connQueryOrPrepared(self, args, 1);
+}
+
 #ifdef DIRECT_ACCESS
 static char connPutLine__doc__[] =
 "putline(line) -- send a line directly to the backend";
@@ -3414,6 +3443,7 @@ static struct PyMethodDef connMethods[] = {
 
 	{"source", (PyCFunction) connSource, METH_NOARGS, connSource__doc__},
 	{"query", (PyCFunction) connQuery, METH_VARARGS, connQuery__doc__},
+	{"execute_prepared", (PyCFunction) connExecPrepared, METH_VARARGS, connExecPrepared__doc__},
 	{"reset", (PyCFunction) connReset, METH_NOARGS, connReset__doc__},
 	{"cancel", (PyCFunction) connCancel, METH_NOARGS, connCancel__doc__},
 	{"close", (PyCFunction) connClose, METH_NOARGS, connClose__doc__},
_______________________________________________
PyGreSQL mailing list
PyGreSQL@Vex.Net
https://mail.vex.net/mailman/listinfo/pygresql

Reply via email to