Merge branch 'master' into fast-codecs

This commit is contained in:
Daniele Varrazzo 2016-12-26 12:06:21 +01:00
commit 7caba160b7
24 changed files with 446 additions and 122 deletions

View File

@ -1,13 +1,24 @@
# Travis CI configuration file for psycopg2
dist: trusty
sudo: required
language: python language: python
python: python:
- 2.6
- 2.7 - 2.7
- 3.6-dev
before_script: - 2.6
- psql -c 'create database psycopg2_test;' -U postgres - 3.5
- 3.4
- 3.3
- 3.2
install: install:
- python setup.py install - python setup.py install
- sudo scripts/travis_prepare.sh
script: make check script:
- scripts/travis_test.sh
notifications:
email: false

3
NEWS
View File

@ -24,6 +24,7 @@ New features:
adapter is deprecated (:tickets:`#317, #343, #387`). adapter is deprecated (:tickets:`#317, #343, #387`).
- Added `~psycopg2.extensions.quote_ident()` function (:ticket:`#359`). - Added `~psycopg2.extensions.quote_ident()` function (:ticket:`#359`).
- Added `~connection.get_dsn_parameters()` connection method (:ticket:`#364`). - Added `~connection.get_dsn_parameters()` connection method (:ticket:`#364`).
- `~cursor.callproc()` now accepts a dictionary of parameters (:ticket:`#381`).
Other changes: Other changes:
@ -36,7 +37,7 @@ What's new in psycopg 2.6.3
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Throw an exception trying to pass ``NULL`` chars as parameters - Throw an exception trying to pass ``NULL`` chars as parameters
(:ticket:`#420). (:ticket:`#420`).
- Make `~psycopg2.extras.Range` objects picklable (:ticket:`#462`). - Make `~psycopg2.extras.Range` objects picklable (:ticket:`#462`).

View File

@ -44,3 +44,8 @@ For any other resource (source code repository, bug tracker, mailing list)
please check the `project homepage`__. please check the `project homepage`__.
.. __: http://initd.org/psycopg/ .. __: http://initd.org/psycopg/
.. image:: https://travis-ci.org/psycopg/psycopg2.svg?branch=master
:target: https://travis-ci.org/psycopg/psycopg2
:alt: Build Status

View File

@ -61,8 +61,8 @@ except ImportError:
release = version release = version
intersphinx_mapping = { intersphinx_mapping = {
'py': ('http://docs.python.org/', None), 'py': ('http://docs.python.org/2', None),
'py3': ('http://docs.python.org/3.4', None), 'py3': ('http://docs.python.org/3', None),
} }
# Pattern to generate links to the bug tracker # Pattern to generate links to the bug tracker

View File

@ -201,13 +201,19 @@ The ``cursor`` class
Call a stored database procedure with the given name. The sequence of Call a stored database procedure with the given name. The sequence of
parameters must contain one entry for each argument that the procedure parameters must contain one entry for each argument that the procedure
expects. The result of the call is returned as modified copy of the expects. Overloaded procedures are supported. Named parameters can be
input sequence. Input parameters are left untouched, output and used by supplying the parameters as a dictionary.
input/output parameters replaced with possibly new values.
The procedure may also provide a result set as output. This must then
be made available through the standard |fetch*|_ methods.
This function is, at present, not DBAPI-compliant. The return value is
supposed to consist of the sequence of parameters with modified output
and input/output parameters. In future versions, the DBAPI-compliant
return value may be implemented, but for now the function returns None.
The procedure may provide a result set as output. This is then made
available through the standard |fetch*|_ methods.
.. versionchanged:: 2.7
added support for named arguments.
.. method:: mogrify(operation [, parameters]) .. method:: mogrify(operation [, parameters])

View File

@ -241,7 +241,7 @@ How do I interrupt a long-running query in an interactive shell?
.. code-block:: pycon .. code-block:: pycon
>>> psycopg2.extensions.set_wait_callback(psycopg2.extensions.wait_select) >>> psycopg2.extensions.set_wait_callback(psycopg2.extras.wait_select)
>>> cnn = psycopg2.connect('') >>> cnn = psycopg2.connect('')
>>> cur = cnn.cursor() >>> cur = cnn.cursor()
>>> cur.execute("select pg_sleep(10)") >>> cur.execute("select pg_sleep(10)")

View File

@ -908,7 +908,7 @@ WHERE typname = 'hstore';
def register_hstore(conn_or_curs, globally=False, unicode=False, def register_hstore(conn_or_curs, globally=False, unicode=False,
oid=None, array_oid=None): oid=None, array_oid=None):
"""Register adapter and typecaster for `!dict`\-\ |hstore| conversions. r"""Register adapter and typecaster for `!dict`\-\ |hstore| conversions.
:param conn_or_curs: a connection or cursor: the typecaster will be :param conn_or_curs: a connection or cursor: the typecaster will be
registered only on this object unless *globally* is set to `!True` registered only on this object unless *globally* is set to `!True`

View File

@ -521,6 +521,25 @@ conn_setup_cancel(connectionObject *self, PGconn *pgconn)
return 0; return 0;
} }
/* Return 1 if the "replication" keyword is set in the DSN, 0 otherwise */
static int
dsn_has_replication(char *pgdsn)
{
int ret = 0;
PQconninfoOption *connopts, *ptr;
connopts = PQconninfoParse(pgdsn, NULL);
for(ptr = connopts; ptr->keyword != NULL; ptr++) {
if(strcmp(ptr->keyword, "replication") == 0 && ptr->val != NULL)
ret = 1;
}
PQconninfoFree(connopts);
return ret;
}
/* Return 1 if the server datestyle allows us to work without problems, /* Return 1 if the server datestyle allows us to work without problems,
0 if it needs to be set to something better, e.g. ISO. */ 0 if it needs to be set to something better, e.g. ISO. */
@ -549,28 +568,29 @@ conn_setup(connectionObject *self, PGconn *pgconn)
{ {
PGresult *pgres = NULL; PGresult *pgres = NULL;
char *error = NULL; char *error = NULL;
int rv = -1;
self->equote = conn_get_standard_conforming_strings(pgconn); self->equote = conn_get_standard_conforming_strings(pgconn);
self->server_version = conn_get_server_version(pgconn); self->server_version = conn_get_server_version(pgconn);
self->protocol = conn_get_protocol_version(self->pgconn); self->protocol = conn_get_protocol_version(self->pgconn);
if (3 != self->protocol) { if (3 != self->protocol) {
PyErr_SetString(InterfaceError, "only protocol 3 supported"); PyErr_SetString(InterfaceError, "only protocol 3 supported");
return -1; goto exit;
} }
if (0 > conn_read_encoding(self, pgconn)) { if (0 > conn_read_encoding(self, pgconn)) {
return -1; goto exit;
} }
if (0 > conn_setup_cancel(self, pgconn)) { if (0 > conn_setup_cancel(self, pgconn)) {
return -1; goto exit;
} }
Py_BEGIN_ALLOW_THREADS; Py_BEGIN_ALLOW_THREADS;
pthread_mutex_lock(&self->lock); pthread_mutex_lock(&self->lock);
Py_BLOCK_THREADS; Py_BLOCK_THREADS;
if (!conn_is_datestyle_ok(self->pgconn)) { if (!dsn_has_replication(self->dsn) && !conn_is_datestyle_ok(self->pgconn)) {
int res; int res;
Py_UNBLOCK_THREADS; Py_UNBLOCK_THREADS;
res = pq_set_guc_locked(self, "datestyle", "ISO", res = pq_set_guc_locked(self, "datestyle", "ISO",
@ -578,18 +598,23 @@ conn_setup(connectionObject *self, PGconn *pgconn)
Py_BLOCK_THREADS; Py_BLOCK_THREADS;
if (res < 0) { if (res < 0) {
pq_complete_error(self, &pgres, &error); pq_complete_error(self, &pgres, &error);
return -1; goto unlock;
} }
} }
/* for reset */ /* for reset */
self->autocommit = 0; self->autocommit = 0;
/* success */
rv = 0;
unlock:
Py_UNBLOCK_THREADS; Py_UNBLOCK_THREADS;
pthread_mutex_unlock(&self->lock); pthread_mutex_unlock(&self->lock);
Py_END_ALLOW_THREADS; Py_END_ALLOW_THREADS;
return 0; exit:
return rv;
} }
/* conn_connect - execute a connection to the database */ /* conn_connect - execute a connection to the database */
@ -886,8 +911,11 @@ _conn_poll_setup_async(connectionObject *self)
self->autocommit = 1; self->autocommit = 1;
/* If the datestyle is ISO or anything else good, /* If the datestyle is ISO or anything else good,
* we can skip the CONN_STATUS_DATESTYLE step. */ * we can skip the CONN_STATUS_DATESTYLE step.
if (!conn_is_datestyle_ok(self->pgconn)) { * Note that we cannot change the datestyle on a replication
* connection.
*/
if (!dsn_has_replication(self->dsn) && !conn_is_datestyle_ok(self->pgconn)) {
Dprintf("conn_poll: status -> CONN_STATUS_DATESTYLE"); Dprintf("conn_poll: status -> CONN_STATUS_DATESTYLE");
self->status = CONN_STATUS_DATESTYLE; self->status = CONN_STATUS_DATESTYLE;
if (0 == pq_send_query(self, psyco_datestyle)) { if (0 == pq_send_query(self, psyco_datestyle)) {

View File

@ -80,6 +80,7 @@ struct cursorObject {
char *qattr; /* quoting attr, used when quoting strings */ char *qattr; /* quoting attr, used when quoting strings */
char *notice; /* a notice from the backend */ char *notice; /* a notice from the backend */
char *name; /* this cursor name */ char *name; /* this cursor name */
char *qname; /* this cursor name, quoted */
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 */

View File

@ -55,7 +55,7 @@ psyco_curs_close(cursorObject *self)
goto exit; goto exit;
} }
if (self->name != NULL) { if (self->qname != NULL) {
char buffer[128]; char buffer[128];
PGTransactionStatusType status; PGTransactionStatusType status;
@ -68,7 +68,7 @@ psyco_curs_close(cursorObject *self)
if (!(status == PQTRANS_UNKNOWN || status == PQTRANS_INERROR)) { if (!(status == PQTRANS_UNKNOWN || status == PQTRANS_INERROR)) {
EXC_IF_NO_MARK(self); EXC_IF_NO_MARK(self);
PyOS_snprintf(buffer, 127, "CLOSE \"%s\"", self->name); PyOS_snprintf(buffer, 127, "CLOSE %s", self->qname);
if (pq_execute(self, buffer, 0, 0, 1) == -1) return NULL; if (pq_execute(self, buffer, 0, 0, 1) == -1) return NULL;
} }
else { else {
@ -422,10 +422,10 @@ _psyco_curs_execute(cursorObject *self,
goto exit; goto exit;
} }
if (self->name != NULL) { if (self->qname != NULL) {
self->query = Bytes_FromFormat( self->query = Bytes_FromFormat(
"DECLARE \"%s\" %sCURSOR %s HOLD FOR %s", "DECLARE %s %sCURSOR %s HOLD FOR %s",
self->name, self->qname,
scroll, scroll,
self->withhold ? "WITH" : "WITHOUT", self->withhold ? "WITH" : "WITHOUT",
Bytes_AS_STRING(fquery)); Bytes_AS_STRING(fquery));
@ -436,10 +436,10 @@ _psyco_curs_execute(cursorObject *self,
} }
} }
else { else {
if (self->name != NULL) { if (self->qname != NULL) {
self->query = Bytes_FromFormat( self->query = Bytes_FromFormat(
"DECLARE \"%s\" %sCURSOR %s HOLD FOR %s", "DECLARE %s %sCURSOR %s HOLD FOR %s",
self->name, self->qname,
scroll, scroll,
self->withhold ? "WITH" : "WITHOUT", self->withhold ? "WITH" : "WITHOUT",
Bytes_AS_STRING(operation)); Bytes_AS_STRING(operation));
@ -768,13 +768,13 @@ psyco_curs_fetchone(cursorObject *self)
if (_psyco_curs_prefetch(self) < 0) return NULL; if (_psyco_curs_prefetch(self) < 0) return NULL;
EXC_IF_NO_TUPLES(self); EXC_IF_NO_TUPLES(self);
if (self->name != NULL) { if (self->qname != NULL) {
char buffer[128]; char buffer[128];
EXC_IF_NO_MARK(self); EXC_IF_NO_MARK(self);
EXC_IF_ASYNC_IN_PROGRESS(self, fetchone); EXC_IF_ASYNC_IN_PROGRESS(self, fetchone);
EXC_IF_TPC_PREPARED(self->conn, fetchone); EXC_IF_TPC_PREPARED(self->conn, fetchone);
PyOS_snprintf(buffer, 127, "FETCH FORWARD 1 FROM \"%s\"", self->name); PyOS_snprintf(buffer, 127, "FETCH FORWARD 1 FROM %s", self->qname);
if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) return NULL; if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) return NULL;
if (_psyco_curs_prefetch(self) < 0) return NULL; if (_psyco_curs_prefetch(self) < 0) return NULL;
} }
@ -823,8 +823,8 @@ psyco_curs_next_named(cursorObject *self)
if (self->row >= self->rowcount) { if (self->row >= self->rowcount) {
char buffer[128]; char buffer[128];
PyOS_snprintf(buffer, 127, "FETCH FORWARD %ld FROM \"%s\"", PyOS_snprintf(buffer, 127, "FETCH FORWARD %ld FROM %s",
self->itersize, self->name); self->itersize, self->qname);
if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) return NULL; if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) return NULL;
if (_psyco_curs_prefetch(self) < 0) return NULL; if (_psyco_curs_prefetch(self) < 0) return NULL;
} }
@ -886,14 +886,14 @@ psyco_curs_fetchmany(cursorObject *self, PyObject *args, PyObject *kwords)
if (_psyco_curs_prefetch(self) < 0) return NULL; if (_psyco_curs_prefetch(self) < 0) return NULL;
EXC_IF_NO_TUPLES(self); EXC_IF_NO_TUPLES(self);
if (self->name != NULL) { if (self->qname != NULL) {
char buffer[128]; char buffer[128];
EXC_IF_NO_MARK(self); EXC_IF_NO_MARK(self);
EXC_IF_ASYNC_IN_PROGRESS(self, fetchmany); EXC_IF_ASYNC_IN_PROGRESS(self, fetchmany);
EXC_IF_TPC_PREPARED(self->conn, fetchone); EXC_IF_TPC_PREPARED(self->conn, fetchone);
PyOS_snprintf(buffer, 127, "FETCH FORWARD %d FROM \"%s\"", PyOS_snprintf(buffer, 127, "FETCH FORWARD %d FROM %s",
(int)size, self->name); (int)size, self->qname);
if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) { goto exit; } if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) { goto exit; }
if (_psyco_curs_prefetch(self) < 0) { goto exit; } if (_psyco_curs_prefetch(self) < 0) { goto exit; }
} }
@ -962,13 +962,13 @@ psyco_curs_fetchall(cursorObject *self)
if (_psyco_curs_prefetch(self) < 0) return NULL; if (_psyco_curs_prefetch(self) < 0) return NULL;
EXC_IF_NO_TUPLES(self); EXC_IF_NO_TUPLES(self);
if (self->name != NULL) { if (self->qname != NULL) {
char buffer[128]; char buffer[128];
EXC_IF_NO_MARK(self); EXC_IF_NO_MARK(self);
EXC_IF_ASYNC_IN_PROGRESS(self, fetchall); EXC_IF_ASYNC_IN_PROGRESS(self, fetchall);
EXC_IF_TPC_PREPARED(self->conn, fetchall); EXC_IF_TPC_PREPARED(self->conn, fetchall);
PyOS_snprintf(buffer, 127, "FETCH FORWARD ALL FROM \"%s\"", self->name); PyOS_snprintf(buffer, 127, "FETCH FORWARD ALL FROM %s", self->qname);
if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) { goto exit; } if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) { goto exit; }
if (_psyco_curs_prefetch(self) < 0) { goto exit; } if (_psyco_curs_prefetch(self) < 0) { goto exit; }
} }
@ -1025,10 +1025,17 @@ psyco_curs_callproc(cursorObject *self, PyObject *args)
PyObject *operation = NULL; PyObject *operation = NULL;
PyObject *res = NULL; PyObject *res = NULL;
if (!PyArg_ParseTuple(args, "s#|O", int using_dict;
&procname, &procname_len, &parameters PyObject *pname = NULL;
)) PyObject *pnames = NULL;
{ goto exit; } PyObject *pvals = NULL;
char *cpname = NULL;
char **scpnames = NULL;
if (!PyArg_ParseTuple(args, "s#|O", &procname, &procname_len,
&parameters)) {
goto exit;
}
EXC_IF_CURS_CLOSED(self); EXC_IF_CURS_CLOSED(self);
EXC_IF_ASYNC_IN_PROGRESS(self, callproc); EXC_IF_ASYNC_IN_PROGRESS(self, callproc);
@ -1036,7 +1043,7 @@ psyco_curs_callproc(cursorObject *self, PyObject *args)
if (self->name != NULL) { if (self->name != NULL) {
psyco_set_error(ProgrammingError, self, psyco_set_error(ProgrammingError, self,
"can't call .callproc() on named cursors"); "can't call .callproc() on named cursors");
goto exit; goto exit;
} }
@ -1044,31 +1051,108 @@ psyco_curs_callproc(cursorObject *self, PyObject *args)
if (-1 == (nparameters = PyObject_Length(parameters))) { goto exit; } if (-1 == (nparameters = PyObject_Length(parameters))) { goto exit; }
} }
/* allocate some memory, build the SQL and create a PyString from it */ using_dict = nparameters > 0 && PyDict_Check(parameters);
sl = procname_len + 17 + nparameters*3 - (nparameters ? 1 : 0);
sql = (char*)PyMem_Malloc(sl); /* a Dict is complicated; the parameter names go into the query */
if (sql == NULL) { if (using_dict) {
PyErr_NoMemory(); if (!(pnames = PyDict_Keys(parameters))) { goto exit; }
if (!(pvals = PyDict_Values(parameters))) { goto exit; }
sl = procname_len + 17 + nparameters * 5 - (nparameters ? 1 : 0);
if (!(scpnames = PyMem_New(char *, nparameters))) {
PyErr_NoMemory();
goto exit;
}
memset(scpnames, 0, sizeof(char *) * nparameters);
/* each parameter has to be processed; it's a few steps. */
for (i = 0; i < nparameters; i++) {
/* all errors are RuntimeErrors as they should never occur */
if (!(pname = PyList_GetItem(pnames, i))) { goto exit; }
Py_INCREF(pname); /* was borrowed */
/* this also makes a check for keys being strings */
if (!(pname = psycopg_ensure_bytes(pname))) { goto exit; }
if (!(cpname = Bytes_AsString(pname))) { goto exit; }
if (!(scpnames[i] = psycopg_escape_identifier(
self->conn, cpname, 0))) {
Py_CLEAR(pname);
goto exit;
}
Py_CLEAR(pname);
sl += strlen(scpnames[i]);
}
if (!(sql = (char*)PyMem_Malloc(sl))) {
PyErr_NoMemory();
goto exit;
}
sprintf(sql, "SELECT * FROM %s(", procname);
for (i = 0; i < nparameters; i++) {
strcat(sql, scpnames[i]);
strcat(sql, ":=%s,");
}
sql[sl-2] = ')';
sql[sl-1] = '\0';
}
/* a list (or None, or empty data structure) is a little bit simpler */
else {
Py_INCREF(parameters);
pvals = parameters;
sl = procname_len + 17 + nparameters * 3 - (nparameters ? 1 : 0);
sql = (char*)PyMem_Malloc(sl);
if (sql == NULL) {
PyErr_NoMemory();
goto exit;
}
sprintf(sql, "SELECT * FROM %s(", procname);
for (i = 0; i < nparameters; i++) {
strcat(sql, "%s,");
}
sql[sl-2] = ')';
sql[sl-1] = '\0';
}
if (!(operation = Bytes_FromString(sql))) {
goto exit; goto exit;
} }
sprintf(sql, "SELECT * FROM %s(", procname); if (0 <= _psyco_curs_execute(
for(i=0; i<nparameters; i++) { self, operation, pvals, self->conn->async, 0)) {
strcat(sql, "%s,"); /* The dict case is outside DBAPI scope anyway, so simply return None */
} if (using_dict) {
sql[sl-2] = ')'; res = Py_None;
sql[sl-1] = '\0'; }
else {
if (!(operation = Bytes_FromString(sql))) { goto exit; } res = pvals;
}
if (0 <= _psyco_curs_execute(self, operation, parameters, Py_INCREF(res);
self->conn->async, 0)) {
Py_INCREF(parameters);
res = parameters;
} }
exit: exit:
if (scpnames != NULL) {
for (i = 0; i < nparameters; i++) {
if (scpnames[i] != NULL) {
PQfreemem(scpnames[i]);
}
}
}
PyMem_Del(scpnames);
Py_XDECREF(pname);
Py_XDECREF(pnames);
Py_XDECREF(operation); Py_XDECREF(operation);
Py_XDECREF(pvals);
PyMem_Free((void*)sql); PyMem_Free((void*)sql);
return res; return res;
} }
@ -1153,7 +1237,7 @@ psyco_curs_scroll(cursorObject *self, PyObject *args, PyObject *kwargs)
/* if the cursor is not named we have the full result set and we can do /* if the cursor is not named we have the full result set and we can do
our own calculations to scroll; else we just delegate the scrolling our own calculations to scroll; else we just delegate the scrolling
to the MOVE SQL statement */ to the MOVE SQL statement */
if (self->name == NULL) { if (self->qname == NULL) {
if (strcmp(mode, "relative") == 0) { if (strcmp(mode, "relative") == 0) {
newpos = self->row + value; newpos = self->row + value;
} else if (strcmp( mode, "absolute") == 0) { } else if (strcmp( mode, "absolute") == 0) {
@ -1181,11 +1265,11 @@ psyco_curs_scroll(cursorObject *self, PyObject *args, PyObject *kwargs)
EXC_IF_TPC_PREPARED(self->conn, scroll); EXC_IF_TPC_PREPARED(self->conn, scroll);
if (strcmp(mode, "absolute") == 0) { if (strcmp(mode, "absolute") == 0) {
PyOS_snprintf(buffer, 127, "MOVE ABSOLUTE %d FROM \"%s\"", PyOS_snprintf(buffer, 127, "MOVE ABSOLUTE %d FROM %s",
value, self->name); value, self->qname);
} }
else { else {
PyOS_snprintf(buffer, 127, "MOVE %d FROM \"%s\"", value, self->name); PyOS_snprintf(buffer, 127, "MOVE %d FROM %s", value, self->qname);
} }
if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) return NULL; if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) return NULL;
if (_psyco_curs_prefetch(self) < 0) return NULL; if (_psyco_curs_prefetch(self) < 0) return NULL;
@ -1815,7 +1899,10 @@ cursor_setup(cursorObject *self, connectionObject *conn, const char *name)
Dprintf("cursor_setup: parameters: name = %s, conn = %p", name, conn); Dprintf("cursor_setup: parameters: name = %s, conn = %p", name, conn);
if (name) { if (name) {
if (!(self->name = psycopg_escape_identifier_easy(name, 0))) { if (0 > psycopg_strdup(&self->name, name, 0)) {
return -1;
}
if (!(self->qname = psycopg_escape_identifier(conn, name, 0))) {
return -1; return -1;
} }
} }
@ -1891,6 +1978,7 @@ cursor_dealloc(PyObject* obj)
cursor_clear(self); cursor_clear(self);
PyMem_Free(self->name); PyMem_Free(self->name);
PQfreemem(self->qname);
CLEARPGRES(self->pgres); CLEARPGRES(self->pgres);

View File

@ -128,7 +128,8 @@ RAISES HIDDEN PyObject *psyco_set_error(PyObject *exc, cursorObject *curs, const
HIDDEN char *psycopg_escape_string(connectionObject *conn, HIDDEN char *psycopg_escape_string(connectionObject *conn,
const char *from, Py_ssize_t len, char *to, Py_ssize_t *tolen); const char *from, Py_ssize_t len, char *to, Py_ssize_t *tolen);
HIDDEN char *psycopg_escape_identifier_easy(const char *from, Py_ssize_t len); HIDDEN char *psycopg_escape_identifier(connectionObject *conn,
const char *str, size_t len);
HIDDEN int psycopg_strdup(char **to, const char *from, Py_ssize_t len); HIDDEN int psycopg_strdup(char **to, const char *from, Py_ssize_t len);
HIDDEN int psycopg_is_text_file(PyObject *f); HIDDEN int psycopg_is_text_file(PyObject *f);

View File

@ -62,7 +62,6 @@
HIDDEN PyObject *pyDateTimeModuleP = NULL; HIDDEN PyObject *pyDateTimeModuleP = NULL;
HIDDEN PyObject *psycoEncodings = NULL; HIDDEN PyObject *psycoEncodings = NULL;
#ifdef PSYCOPG_DEBUG #ifdef PSYCOPG_DEBUG
HIDDEN int psycopg_debug_enabled = 0; HIDDEN int psycopg_debug_enabled = 0;
#endif #endif
@ -191,9 +190,8 @@ psyco_quote_ident(PyObject *self, PyObject *args, PyObject *kwargs)
str = Bytes_AS_STRING(ident); str = Bytes_AS_STRING(ident);
quoted = PQescapeIdentifier(conn->pgconn, str, strlen(str)); quoted = psycopg_escape_identifier(conn, str, strlen(str));
if (!quoted) { if (!quoted) {
PyErr_NoMemory();
goto exit; goto exit;
} }
result = conn_text_from_chars(conn, quoted); result = conn_text_from_chars(conn, quoted);

View File

@ -90,43 +90,40 @@ psycopg_escape_string(connectionObject *conn, const char *from, Py_ssize_t len,
return to; return to;
} }
/* Escape a string to build a valid PostgreSQL identifier. /* Escape a string for inclusion in a query as identifier.
* *
* Allocate a new buffer on the Python heap containing the new string.
* 'len' is optional: if 0 the length is calculated. * 'len' is optional: if 0 the length is calculated.
* *
* The returned string doesn't include quotes. * Return a string allocated by Postgres: free it using PQfreemem
* * In case of error set a Python exception.
* WARNING: this function is not so safe to allow untrusted input: it does no
* check for multibyte chars. Such a function should be built on
* PQescapeIdentifier, which is only available from PostgreSQL 9.0.
*/ */
char * char *
psycopg_escape_identifier_easy(const char *from, Py_ssize_t len) psycopg_escape_identifier(connectionObject *conn, const char *str, size_t len)
{ {
char *rv; char *rv = NULL;
const char *src;
char *dst;
if (!len) { len = strlen(from); } if (!conn || !conn->pgconn) {
if (!(rv = PyMem_New(char, 1 + 2 * len))) { PyErr_SetString(InterfaceError, "connection not valid");
PyErr_NoMemory(); goto exit;
return NULL;
} }
/* The only thing to do is double quotes */ if (!len) { len = strlen(str); }
for (src = from, dst = rv; *src; ++src, ++dst) {
*dst = *src; rv = PQescapeIdentifier(conn->pgconn, str, len);
if ('"' == *src) { if (!rv) {
*++dst = '"'; char *msg;
msg = PQerrorMessage(conn->pgconn);
if (!msg || !msg[0]) {
msg = "no message provided";
} }
PyErr_Format(InterfaceError, "failed to escape identifier: %s", msg);
} }
*dst = '\0'; exit:
return rv; return rv;
} }
/* Duplicate a string. /* Duplicate a string.
* *
* Allocate a new buffer on the Python heap containing the new string. * Allocate a new buffer on the Python heap containing the new string.

60
scripts/travis_prepare.sh Executable file
View File

@ -0,0 +1,60 @@
#!/bin/bash
set -e
# Prepare the test databases in Travis CI.
# The script should be run with sudo.
# The script is not idempotent: it assumes the machine in a clean state
# and is designed for a sudo-enabled Trusty environment.
set_param () {
# Set a parameter in a postgresql.conf file
version=$1
param=$2
value=$3
sed -i "s/^\s*#\?\s*$param.*/$param = $value/" \
"/etc/postgresql/$version/psycopg/postgresql.conf"
}
create () {
version=$1
port=$2
dbname=psycopg2_test
pg_createcluster -p $port --start-conf manual $version psycopg
# for two-phase commit testing
set_param "$version" max_prepared_transactions 10
# for replication testing
set_param "$version" max_wal_senders 5
set_param "$version" max_replication_slots 5
if [ "$version" == "9.2" -o "$version" == "9.3" ]
then
set_param "$version" wal_level hot_standby
else
set_param "$version" wal_level logical
fi
echo "local replication travis trust" \
>> "/etc/postgresql/$version/psycopg/pg_hba.conf"
pg_ctlcluster "$version" psycopg start
sudo -u postgres psql -c "create user travis replication" "port=$port"
sudo -u postgres psql -c "create database $dbname" "port=$port"
sudo -u postgres psql -c "grant create on database $dbname to travis" "port=$port"
sudo -u postgres psql -c "create extension hstore" "port=$port dbname=$dbname"
}
# Would give a permission denied error in the travis build dir
cd /
create 9.6 54396
create 9.5 54395
create 9.4 54394
create 9.3 54393
create 9.2 54392

30
scripts/travis_test.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash
# Run the tests in all the databases
# The script is designed for a Trusty environment.
set -e
run_test () {
version=$1
port=$2
dbname=psycopg2_test
printf "\n\nRunning tests against PostgreSQL $version\n\n"
export PSYCOPG2_TESTDB=$dbname
export PSYCOPG2_TESTDB_PORT=$port
export PSYCOPG2_TESTDB_USER=travis
export PSYCOPG2_TEST_REPL_DSN=
unset PSYCOPG2_TEST_GREEN
python -c "from psycopg2 import tests; tests.unittest.main(defaultTest='tests.test_suite')"
printf "\n\nRunning tests against PostgreSQL $version (green mode)\n\n"
export PSYCOPG2_TEST_GREEN=1
python -c "from psycopg2 import tests; tests.unittest.main(defaultTest='tests.test_suite')"
}
run_test 9.6 54396
run_test 9.5 54395
run_test 9.4 54394
run_test 9.3 54393
run_test 9.2 54392

View File

@ -7,24 +7,23 @@ define=
# "pg_config" is required to locate PostgreSQL headers and libraries needed to # "pg_config" is required to locate PostgreSQL headers and libraries needed to
# build psycopg2. If pg_config is not in the path or is installed under a # build psycopg2. If pg_config is not in the path or is installed under a
# different name uncomment the following option and set it to the pg_config # different name set the following option to the pg_config full path.
# full path. pg_config=
#pg_config=
# Set to 1 to use Python datetime objects for default date/time representation. # Set to 1 to use Python datetime objects for default date/time representation.
use_pydatetime=1 use_pydatetime=1
# If the build system does not find the mx.DateTime headers, try # If the build system does not find the mx.DateTime headers, try
# uncommenting the following line and setting its value to the right path. # setting its value to the right path.
#mx_include_dir= mx_include_dir=
# For Windows only: # For Windows only:
# Set to 1 if the PostgreSQL library was built with OpenSSL. # Set to 1 if the PostgreSQL library was built with OpenSSL.
# Required to link in OpenSSL libraries and dependencies. # Required to link in OpenSSL libraries and dependencies.
have_ssl=0 have_ssl=0
# Statically link against the postgresql client library. # Set to 1 to statically link against the postgresql client library.
#static_libpq=1 static_libpq=0
# Add here eventual extra libraries required to link the module. # Add here eventual extra libraries required to link the module.
#libraries= libraries=

View File

@ -381,6 +381,11 @@ class psycopg_build_ext(build_ext):
def finalize_options(self): def finalize_options(self):
"""Complete the build system configuration.""" """Complete the build system configuration."""
# An empty option in the setup.cfg causes self.libraries to include
# an empty string in the list of libraries
if self.libraries is not None and not self.libraries.strip():
self.libraries = None
build_ext.finalize_options(self) build_ext.finalize_options(self)
pg_config_helper = PostgresConfig(self) pg_config_helper = PostgresConfig(self)
@ -521,7 +526,7 @@ if parser.has_option('build_ext', 'mx_include_dir'):
mxincludedir = parser.get('build_ext', 'mx_include_dir') mxincludedir = parser.get('build_ext', 'mx_include_dir')
else: else:
mxincludedir = os.path.join(get_python_inc(plat_specific=1), "mx") mxincludedir = os.path.join(get_python_inc(plat_specific=1), "mx")
if os.path.exists(mxincludedir): if mxincludedir.strip() and os.path.exists(mxincludedir):
# Build the support for mx: we will check at runtime if it can be imported # Build the support for mx: we will check at runtime if it can be imported
include_dirs.append(mxincludedir) include_dirs.append(mxincludedir)
define_macros.append(('HAVE_MXDATETIME', '1')) define_macros.append(('HAVE_MXDATETIME', '1'))

View File

@ -465,7 +465,7 @@ class MakeDsnTestCase(ConnectingTestCase):
conn = self.connect() conn = self.connect()
d = conn.get_dsn_parameters() d = conn.get_dsn_parameters()
self.assertEqual(d['dbname'], dbname) # the only param we can check reliably self.assertEqual(d['dbname'], dbname) # the only param we can check reliably
self.assertNotIn('password', d) self.assert_('password' not in d, d)
class IsolationLevelsTestCase(ConnectingTestCase): class IsolationLevelsTestCase(ConnectingTestCase):

View File

@ -498,6 +498,48 @@ class CursorTests(ConnectingTestCase):
cur = self.conn.cursor() cur = self.conn.cursor()
self.assertRaises(TypeError, cur.callproc, 'lower', 42) self.assertRaises(TypeError, cur.callproc, 'lower', 42)
# It would be inappropriate to test callproc's named parameters in the
# DBAPI2.0 test section because they are a psycopg2 extension.
@skip_before_postgres(9, 0)
def test_callproc_dict(self):
# This parameter name tests for injection and quote escaping
paramname = '''
Robert'); drop table "students" --
'''.strip()
escaped_paramname = '"%s"' % paramname.replace('"', '""')
procname = 'pg_temp.randall'
cur = self.conn.cursor()
# Set up the temporary function
cur.execute('''
CREATE FUNCTION %s(%s INT)
RETURNS INT AS
'SELECT $1 * $1'
LANGUAGE SQL
''' % (procname, escaped_paramname));
# Make sure callproc works right
cur.callproc(procname, { paramname: 2 })
self.assertEquals(cur.fetchone()[0], 4)
# Make sure callproc fails right
failing_cases = [
({ paramname: 2, 'foo': 'bar' }, psycopg2.ProgrammingError),
({ paramname: '2' }, psycopg2.ProgrammingError),
({ paramname: 'two' }, psycopg2.ProgrammingError),
({ u'bj\xc3rn': 2 }, psycopg2.ProgrammingError),
({ 3: 2 }, TypeError),
({ self: 2 }, TypeError),
]
for parameter_sequence, exception in failing_cases:
self.assertRaises(exception, cur.callproc, procname, parameter_sequence)
self.conn.rollback()
def test_callproc_badparam(self):
cur = self.conn.cursor()
self.assertRaises(TypeError, cur.callproc, 'lower', 42)
def test_suite(): def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__) return unittest.TestLoader().loadTestsFromName(__name__)

View File

@ -119,8 +119,8 @@ class ConnectTestCase(unittest.TestCase):
def test_int_port_param(self): def test_int_port_param(self):
psycopg2.connect(database='sony', port=6543) psycopg2.connect(database='sony', port=6543)
dsn = " %s " % self.args[0] dsn = " %s " % self.args[0]
self.assertIn(" dbname=sony ", dsn) self.assert_(" dbname=sony " in dsn, dsn)
self.assertIn(" port=6543 ", dsn) self.assert_(" port=6543 " in dsn, dsn)
def test_empty_param(self): def test_empty_param(self):
psycopg2.connect(database='sony', password='') psycopg2.connect(database='sony', password='')

View File

@ -36,7 +36,31 @@ class Psycopg2Tests(dbapi20.DatabaseAPI20Test):
connect_args = () connect_args = ()
connect_kw_args = {'dsn': dsn} connect_kw_args = {'dsn': dsn}
lower_func = 'lower' # For stored procedure test lower_func = 'lower' # For stored procedure test
def test_callproc(self):
# Until DBAPI 2.0 compliance, callproc should return None or it's just
# misleading. Therefore, we will skip the return value test for
# callproc and only perform the fetch test.
#
# For what it's worth, the DBAPI2.0 test_callproc doesn't actually
# test for DBAPI2.0 compliance! It doesn't check for modified OUT and
# IN/OUT parameters in the return values!
con = self._connect()
try:
cur = con.cursor()
if self.lower_func and hasattr(cur,'callproc'):
cur.callproc(self.lower_func,('FOO',))
r = cur.fetchall()
self.assertEqual(len(r),1,'callproc produced no result set')
self.assertEqual(len(r[0]),1,
'callproc produced invalid result set'
)
self.assertEqual(r[0][0],'foo',
'callproc produced invalid results'
)
finally:
con.close()
def test_setoutputsize(self): def test_setoutputsize(self):
# psycopg2's setoutputsize() is a no-op # psycopg2's setoutputsize() is a no-op

View File

@ -65,11 +65,13 @@ class QuotingTestCase(ConnectingTestCase):
curs = self.conn.cursor() curs = self.conn.cursor()
data = 'abcd\x01\x00cdefg' data = 'abcd\x01\x00cdefg'
with self.assertRaises(ValueError) as e: try:
curs.execute("SELECT %s", (data,)) curs.execute("SELECT %s", (data,))
except ValueError as e:
self.assertEquals(str(e.exception), self.assertEquals(str(e),
'A string literal cannot contain NUL (0x00) characters.') 'A string literal cannot contain NUL (0x00) characters.')
else:
self.fail("ValueError not raised")
def test_binary(self): def test_binary(self):
data = b"""some data with \000\013 binary data = b"""some data with \000\013 binary

32
tests/test_replication.py Normal file → Executable file
View File

@ -23,23 +23,19 @@
# License for more details. # License for more details.
import psycopg2 import psycopg2
import psycopg2.extensions
from psycopg2.extras import ( from psycopg2.extras import (
PhysicalReplicationConnection, LogicalReplicationConnection, StopReplication) PhysicalReplicationConnection, LogicalReplicationConnection, StopReplication)
import testconfig import testconfig
from testutils import unittest from testutils import unittest, ConnectingTestCase
from testutils import skip_before_postgres from testutils import skip_before_postgres, skip_if_green
from testutils import ConnectingTestCase
skip_repl_if_green = skip_if_green("replication not supported in green mode")
class ReplicationTestCase(ConnectingTestCase): class ReplicationTestCase(ConnectingTestCase):
def setUp(self): def setUp(self):
if not testconfig.repl_dsn:
self.skipTest("replication tests disabled by default")
super(ReplicationTestCase, self).setUp() super(ReplicationTestCase, self).setUp()
self.slot = testconfig.repl_slot self.slot = testconfig.repl_slot
self._slots = [] self._slots = []
@ -93,6 +89,20 @@ class ReplicationTest(ReplicationTestCase):
cur.execute("IDENTIFY_SYSTEM") cur.execute("IDENTIFY_SYSTEM")
cur.fetchall() cur.fetchall()
@skip_before_postgres(9, 0)
def test_datestyle(self):
if testconfig.repl_dsn is None:
return self.skipTest("replication tests disabled by default")
conn = self.repl_connect(
dsn=testconfig.repl_dsn, options='-cdatestyle=german',
connection_factory=PhysicalReplicationConnection)
if conn is None:
return
cur = conn.cursor()
cur.execute("IDENTIFY_SYSTEM")
cur.fetchall()
@skip_before_postgres(9, 4) @skip_before_postgres(9, 4)
def test_logical_replication_connection(self): def test_logical_replication_connection(self):
conn = self.repl_connect(connection_factory=LogicalReplicationConnection) conn = self.repl_connect(connection_factory=LogicalReplicationConnection)
@ -114,6 +124,7 @@ class ReplicationTest(ReplicationTestCase):
psycopg2.ProgrammingError, self.create_replication_slot, cur) psycopg2.ProgrammingError, self.create_replication_slot, cur)
@skip_before_postgres(9, 4) # slots require 9.4 @skip_before_postgres(9, 4) # slots require 9.4
@skip_repl_if_green
def test_start_on_missing_replication_slot(self): def test_start_on_missing_replication_slot(self):
conn = self.repl_connect(connection_factory=PhysicalReplicationConnection) conn = self.repl_connect(connection_factory=PhysicalReplicationConnection)
if conn is None: if conn is None:
@ -127,6 +138,7 @@ class ReplicationTest(ReplicationTestCase):
cur.start_replication(self.slot) cur.start_replication(self.slot)
@skip_before_postgres(9, 4) # slots require 9.4 @skip_before_postgres(9, 4) # slots require 9.4
@skip_repl_if_green
def test_start_and_recover_from_error(self): def test_start_and_recover_from_error(self):
conn = self.repl_connect(connection_factory=LogicalReplicationConnection) conn = self.repl_connect(connection_factory=LogicalReplicationConnection)
if conn is None: if conn is None:
@ -148,6 +160,7 @@ class ReplicationTest(ReplicationTestCase):
cur.start_replication(slot_name=self.slot) cur.start_replication(slot_name=self.slot)
@skip_before_postgres(9, 4) # slots require 9.4 @skip_before_postgres(9, 4) # slots require 9.4
@skip_repl_if_green
def test_stop_replication(self): def test_stop_replication(self):
conn = self.repl_connect(connection_factory=LogicalReplicationConnection) conn = self.repl_connect(connection_factory=LogicalReplicationConnection)
if conn is None: if conn is None:
@ -167,12 +180,13 @@ class ReplicationTest(ReplicationTestCase):
class AsyncReplicationTest(ReplicationTestCase): class AsyncReplicationTest(ReplicationTestCase):
@skip_before_postgres(9, 4) # slots require 9.4 @skip_before_postgres(9, 4) # slots require 9.4
@skip_repl_if_green
def test_async_replication(self): def test_async_replication(self):
conn = self.repl_connect( conn = self.repl_connect(
connection_factory=LogicalReplicationConnection, async=1) connection_factory=LogicalReplicationConnection, async=1)
if conn is None: if conn is None:
return return
self.wait(conn)
cur = conn.cursor() cur = conn.cursor()
self.create_replication_slot(cur, output_plugin='test_decoding') self.create_replication_slot(cur, output_plugin='test_decoding')

View File

@ -122,13 +122,25 @@ class ConnectingTestCase(unittest.TestCase):
Should raise a skip test if not available, but guard for None on Should raise a skip test if not available, but guard for None on
old Python versions. old Python versions.
""" """
if repl_dsn is None:
return self.skipTest("replication tests disabled by default")
if 'dsn' not in kwargs: if 'dsn' not in kwargs:
kwargs['dsn'] = repl_dsn kwargs['dsn'] = repl_dsn
import psycopg2 import psycopg2
try: try:
conn = self.connect(**kwargs) conn = self.connect(**kwargs)
if conn.async == 1:
self.wait(conn)
except psycopg2.OperationalError, e: except psycopg2.OperationalError, e:
return self.skipTest("replication db not configured: %s" % e) # If pgcode is not set it is a genuine connection error
# Otherwise we tried to run some bad operation in the connection
# (e.g. bug #482) and we'd rather know that.
if e.pgcode is None:
return self.skipTest("replication db not configured: %s" % e)
else:
raise
return conn return conn
def _get_conn(self): def _get_conn(self):