Use escape string syntax for string escape if connected to a server

requiring it.

Added a connection flag to store whether E''-style quoting is required: this
avoids repeated PQparameterStatus() calls.

Added a test case to verify correct behavior on strings, unicode and binary 
data. Tested with PG versions from 7.4 to 8.3b2, with any server
'standard_conforming_strings' setting and with 'PSYCOPG_OWN_QUOTING' too.
This commit is contained in:
Daniele Varrazzo 2007-11-11 08:53:44 +00:00
parent a6ea092acc
commit 75cb5d75d7
6 changed files with 127 additions and 32 deletions

View File

@ -1,3 +1,8 @@
2007-11-11 Daniele Varrazzo <daniele.varrazzo@gmail.com>
* Use escape string syntax for string escape if connected to a
server requiring it.
2007-11-09 Daniele Varrazzo <daniele.varrazzo@gmail.com>
* Use escape string syntax for binary escape if connected to a

View File

@ -137,46 +137,23 @@ binary_quote(binaryObject *self)
const char *buffer;
Py_ssize_t buffer_len;
size_t len = 0;
PGconn *pgconn = NULL;
const char *quotes = "'%s'";
const char *scs;
/* if we got a plain string or a buffer we escape it and save the buffer */
if (PyString_Check(self->wrapped) || PyBuffer_Check(self->wrapped)) {
/* escape and build quoted buffer */
PyObject_AsCharBuffer(self->wrapped, &buffer, &buffer_len);
if (self->conn) {
pgconn = ((connectionObject*)self->conn)->pgconn;
/*
* The presence of the 'standard_conforming_strings' parameters
* means that the server _accepts_ the E'' quote.
*
* If the paramer is off, the PQescapeByteaConn returns
* backslash escaped strings (e.g. '\001' -> "\\001"),
* so the E'' quotes are required to avoid warnings
* if 'escape_string_warning' is set.
*
* If the parameter is on, the PQescapeByteaConn returns
* not escaped strings (e.g. '\001' -> "\001"), relying on the
* fact that the '\' will pass untouched the string parser.
* In this case the E'' quotes are NOT to be used.
*/
scs = PQparameterStatus(pgconn, "standard_conforming_strings");
if (scs && (0 == strcmp("off", scs)))
quotes = "E'%s'";
}
to = (char *)binary_escape((unsigned char*)buffer, (size_t) buffer_len,
&len, pgconn);
&len, self->conn ? ((connectionObject*)self->conn)->pgconn : NULL);
if (to == NULL) {
PyErr_NoMemory();
return NULL;
}
if (len > 0)
self->buffer = PyString_FromFormat(quotes, to);
self->buffer = PyString_FromFormat(
(self->conn && ((connectionObject*)self->conn)->equote)
? "E'%s'" : "'%s'" , to);
else
self->buffer = PyString_FromString("''");

View File

@ -94,6 +94,7 @@ qstring_quote(qstringObject *self)
PyObject *str;
char *s, *buffer;
Py_ssize_t len;
int equote; /* buffer offset if E'' quotes are needed */
/* if the wrapped object is an unicode object we can encode it to match
self->encoding but if the encoding is not specified we don't know what
@ -141,20 +142,22 @@ qstring_quote(qstringObject *self)
/* encode the string into buffer */
PyString_AsStringAndSize(str, &s, &len);
buffer = (char *)PyMem_Malloc((len*2+3) * sizeof(char));
buffer = (char *)PyMem_Malloc((len*2+4) * sizeof(char));
if (buffer == NULL) {
Py_DECREF(str);
PyErr_NoMemory();
return NULL;
}
equote = (self->conn && ((connectionObject*)self->conn)->equote) ? 1 : 0;
{ /* Call qstring_escape with the GIL released, then reacquire the GIL
* before verifying that the results can fit into a Python string; raise
* an exception if not. */
size_t qstring_res;
Py_BEGIN_ALLOW_THREADS
qstring_res = qstring_escape(buffer+1, s, len,
qstring_res = qstring_escape(buffer+equote+1, s, len,
self->conn ? ((connectionObject*)self->conn)->pgconn : NULL);
Py_END_ALLOW_THREADS
@ -166,10 +169,12 @@ qstring_quote(qstringObject *self)
return NULL;
}
len = (Py_ssize_t) qstring_res;
buffer[0] = '\'' ; buffer[len+1] = '\'';
if (equote)
buffer[0] = 'E';
buffer[equote] = '\'' ; buffer[len+equote+1] = '\'';
}
self->buffer = PyString_FromStringAndSize(buffer, len+2);
self->buffer = PyString_FromStringAndSize(buffer, len+equote+2);
PyMem_Free(buffer);
Py_DECREF(str);

View File

@ -83,6 +83,8 @@ typedef struct {
PyObject *string_types; /* a set of typecasters for string types */
PyObject *binary_types; /* a set of typecasters for binary types */
int equote; /* use E''-style quotes for escaped strings */
} connectionObject;
/* C-callable functions in connection_int.c and connection_ext.c */

View File

@ -54,7 +54,7 @@ conn_notice_callback(void *args, const char *message)
}
}
/* conn_connect - execute a connection to the dataabase */
/* conn_connect - execute a connection to the database */
int
conn_connect(connectionObject *self)
@ -62,6 +62,7 @@ conn_connect(connectionObject *self)
PGconn *pgconn;
PGresult *pgres;
char *data, *tmp;
const char *scs; /* standard-conforming strings */
size_t i;
/* we need the initial date style to be ISO, for typecasters; if the user
@ -97,6 +98,34 @@ conn_connect(connectionObject *self)
PQsetNoticeProcessor(pgconn, conn_notice_callback, (void*)self);
/*
* The presence of the 'standard_conforming_strings' parameter
* means that the server _accepts_ the E'' quote.
*
* If the paramer is off, the PQescapeByteaConn returns
* backslash escaped strings (e.g. '\001' -> "\\001"),
* so the E'' quotes are required to avoid warnings
* if 'escape_string_warning' is set.
*
* If the parameter is on, the PQescapeByteaConn returns
* not escaped strings (e.g. '\001' -> "\001"), relying on the
* fact that the '\' will pass untouched the string parser.
* In this case the E'' quotes are NOT to be used.
*
* The PSYCOPG_OWN_QUOTING implementation always returns escaped strings.
*/
scs = PQparameterStatus(pgconn, "standard_conforming_strings");
Dprintf("conn_connect: server standard_conforming_strings parameter: %s",
scs ? scs : "unavailable");
#ifndef PSYCOPG_OWN_QUOTING
self->equote = (scs && (0 == strcmp("off", scs)));
#else
self->equote = (scs != NULL);
#endif
Dprintf("conn_connect: server requires E'' quotes: %s",
self->equote ? "YES" : "NO");
Py_BEGIN_ALLOW_THREADS;
pgres = PQexec(pgconn, datestyle);
Py_END_ALLOW_THREADS;

77
tests/test_quote.py Normal file
View File

@ -0,0 +1,77 @@
import psycopg2
import psycopg2.extensions
import unittest
import tests
class QuotingTestCase(unittest.TestCase):
r"""Checks the correct quoting of strings and binary objects.
Since ver. 8.1, PostgreSQL is moving towards SQL standard conforming
strings, where the backslash (\) is treated as literal character,
not as escape. To treat the backslash as a C-style escapes, PG supports
the E'' quotes.
This test case checks that the E'' quotes are used whenever they are
needed. The tests are expected to pass with all PostgreSQL server versions
(currently tested with 7.4 <= PG <= 8.3beta) and with any
'standard_conforming_strings' server parameter value.
The tests also check that no warning is raised ('escape_string_warning'
should be on).
http://www.postgresql.org/docs/8.1/static/sql-syntax.html#SQL-SYNTAX-STRINGS
http://www.postgresql.org/docs/8.1/static/runtime-config-compatible.html
"""
def setUp(self):
self.conn = psycopg2.connect("dbname=%s" % tests.dbname)
def tearDown(self):
self.conn.close()
def test_string(self):
data = """some data with \t chars
to escape into, 'quotes' and \\ a backslash too.
"""
data += "".join(map(chr, range(1,127)))
curs = self.conn.cursor()
curs.execute("SELECT %s;", (data,))
res = curs.fetchone()[0]
self.assertEqual(res, data)
self.assert_(not self.conn.notices)
def test_binary(self):
data = """some data with \000\013 binary
stuff into, 'quotes' and \\ a backslash too.
"""
data += "".join(map(chr, range(256)))
curs = self.conn.cursor()
curs.execute("SELECT %s::bytea;", (psycopg2.Binary(data),))
res = str(curs.fetchone()[0])
self.assertEqual(res, data)
self.assert_(not self.conn.notices)
def test_unicode(self):
data = u"""some data with \t chars
to escape into, 'quotes', \u20ac euro sign and \\ a backslash too.
"""
data += u"".join(map(unichr, [ u for u in range(1,65536)
if not 0xD800 <= u <= 0xDFFF ])) # surrogate area
self.conn.set_client_encoding('UNICODE')
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
curs = self.conn.cursor()
curs.execute("SELECT %s::text;", (data,))
res = curs.fetchone()[0]
self.assertEqual(res, data)
self.assert_(not self.conn.notices)
def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == "__main__":
unittest.main()