mirror of
https://github.com/psycopg/psycopg2.git
synced 2025-01-31 09:24:07 +03:00
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:
parent
a6ea092acc
commit
75cb5d75d7
|
@ -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>
|
2007-11-09 Daniele Varrazzo <daniele.varrazzo@gmail.com>
|
||||||
|
|
||||||
* Use escape string syntax for binary escape if connected to a
|
* Use escape string syntax for binary escape if connected to a
|
||||||
|
|
|
@ -137,46 +137,23 @@ binary_quote(binaryObject *self)
|
||||||
const char *buffer;
|
const char *buffer;
|
||||||
Py_ssize_t buffer_len;
|
Py_ssize_t buffer_len;
|
||||||
size_t len = 0;
|
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 we got a plain string or a buffer we escape it and save the buffer */
|
||||||
if (PyString_Check(self->wrapped) || PyBuffer_Check(self->wrapped)) {
|
if (PyString_Check(self->wrapped) || PyBuffer_Check(self->wrapped)) {
|
||||||
/* escape and build quoted buffer */
|
/* escape and build quoted buffer */
|
||||||
PyObject_AsCharBuffer(self->wrapped, &buffer, &buffer_len);
|
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,
|
to = (char *)binary_escape((unsigned char*)buffer, (size_t) buffer_len,
|
||||||
&len, pgconn);
|
&len, self->conn ? ((connectionObject*)self->conn)->pgconn : NULL);
|
||||||
if (to == NULL) {
|
if (to == NULL) {
|
||||||
PyErr_NoMemory();
|
PyErr_NoMemory();
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 0)
|
if (len > 0)
|
||||||
self->buffer = PyString_FromFormat(quotes, to);
|
self->buffer = PyString_FromFormat(
|
||||||
|
(self->conn && ((connectionObject*)self->conn)->equote)
|
||||||
|
? "E'%s'" : "'%s'" , to);
|
||||||
else
|
else
|
||||||
self->buffer = PyString_FromString("''");
|
self->buffer = PyString_FromString("''");
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@ qstring_quote(qstringObject *self)
|
||||||
PyObject *str;
|
PyObject *str;
|
||||||
char *s, *buffer;
|
char *s, *buffer;
|
||||||
Py_ssize_t len;
|
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
|
/* 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
|
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 */
|
/* encode the string into buffer */
|
||||||
PyString_AsStringAndSize(str, &s, &len);
|
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) {
|
if (buffer == NULL) {
|
||||||
Py_DECREF(str);
|
Py_DECREF(str);
|
||||||
PyErr_NoMemory();
|
PyErr_NoMemory();
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
equote = (self->conn && ((connectionObject*)self->conn)->equote) ? 1 : 0;
|
||||||
|
|
||||||
{ /* Call qstring_escape with the GIL released, then reacquire the GIL
|
{ /* Call qstring_escape with the GIL released, then reacquire the GIL
|
||||||
* before verifying that the results can fit into a Python string; raise
|
* before verifying that the results can fit into a Python string; raise
|
||||||
* an exception if not. */
|
* an exception if not. */
|
||||||
size_t qstring_res;
|
size_t qstring_res;
|
||||||
|
|
||||||
Py_BEGIN_ALLOW_THREADS
|
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);
|
self->conn ? ((connectionObject*)self->conn)->pgconn : NULL);
|
||||||
Py_END_ALLOW_THREADS
|
Py_END_ALLOW_THREADS
|
||||||
|
|
||||||
|
@ -166,10 +169,12 @@ qstring_quote(qstringObject *self)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
len = (Py_ssize_t) qstring_res;
|
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);
|
PyMem_Free(buffer);
|
||||||
Py_DECREF(str);
|
Py_DECREF(str);
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,8 @@ typedef struct {
|
||||||
PyObject *string_types; /* a set of typecasters for string types */
|
PyObject *string_types; /* a set of typecasters for string types */
|
||||||
PyObject *binary_types; /* a set of typecasters for binary types */
|
PyObject *binary_types; /* a set of typecasters for binary types */
|
||||||
|
|
||||||
|
int equote; /* use E''-style quotes for escaped strings */
|
||||||
|
|
||||||
} connectionObject;
|
} connectionObject;
|
||||||
|
|
||||||
/* C-callable functions in connection_int.c and connection_ext.c */
|
/* C-callable functions in connection_int.c and connection_ext.c */
|
||||||
|
|
|
@ -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
|
int
|
||||||
conn_connect(connectionObject *self)
|
conn_connect(connectionObject *self)
|
||||||
|
@ -62,6 +62,7 @@ conn_connect(connectionObject *self)
|
||||||
PGconn *pgconn;
|
PGconn *pgconn;
|
||||||
PGresult *pgres;
|
PGresult *pgres;
|
||||||
char *data, *tmp;
|
char *data, *tmp;
|
||||||
|
const char *scs; /* standard-conforming strings */
|
||||||
size_t i;
|
size_t i;
|
||||||
|
|
||||||
/* we need the initial date style to be ISO, for typecasters; if the user
|
/* 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);
|
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;
|
Py_BEGIN_ALLOW_THREADS;
|
||||||
pgres = PQexec(pgconn, datestyle);
|
pgres = PQexec(pgconn, datestyle);
|
||||||
Py_END_ALLOW_THREADS;
|
Py_END_ALLOW_THREADS;
|
||||||
|
|
77
tests/test_quote.py
Normal file
77
tests/test_quote.py
Normal 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()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user