diff options
author | Daniele Varrazzo <daniele.varrazzo@gmail.com> | 2016-12-26 12:06:21 +0100 |
---|---|---|
committer | Daniele Varrazzo <daniele.varrazzo@gmail.com> | 2016-12-26 12:06:21 +0100 |
commit | 7caba160b7083c64197329e17d0d0e0eb17c8639 (patch) | |
tree | 9c7a8221ccdbbf3efdbac0ccd49287cc90647a6f | |
parent | 121cf3b8f8426765d983579d3a4b2e932429cd9f (diff) | |
parent | e9577e9b890fd9a27bb146e8ea1c24eb562f28b2 (diff) | |
download | psycopg2-7caba160b7083c64197329e17d0d0e0eb17c8639.tar.gz |
Merge branch 'master' into fast-codecs
-rw-r--r-- | .travis.yml | 21 | ||||
-rw-r--r-- | NEWS | 3 | ||||
-rw-r--r-- | README.rst | 5 | ||||
-rw-r--r-- | doc/src/conf.py | 4 | ||||
-rw-r--r-- | doc/src/cursor.rst | 18 | ||||
-rw-r--r-- | doc/src/faq.rst | 2 | ||||
-rw-r--r-- | lib/extras.py | 2 | ||||
-rw-r--r-- | psycopg/connection_int.c | 44 | ||||
-rw-r--r-- | psycopg/cursor.h | 1 | ||||
-rw-r--r-- | psycopg/cursor_type.c | 174 | ||||
-rw-r--r-- | psycopg/psycopg.h | 3 | ||||
-rw-r--r-- | psycopg/psycopgmodule.c | 4 | ||||
-rw-r--r-- | psycopg/utils.c | 41 | ||||
-rwxr-xr-x | scripts/travis_prepare.sh | 60 | ||||
-rwxr-xr-x | scripts/travis_test.sh | 30 | ||||
-rw-r--r-- | setup.cfg | 15 | ||||
-rw-r--r-- | setup.py | 7 | ||||
-rwxr-xr-x | tests/test_connection.py | 2 | ||||
-rwxr-xr-x | tests/test_cursor.py | 42 | ||||
-rwxr-xr-x | tests/test_module.py | 4 | ||||
-rwxr-xr-x | tests/test_psycopg2_dbapi20.py | 26 | ||||
-rwxr-xr-x | tests/test_quote.py | 10 | ||||
-rwxr-xr-x[-rw-r--r--] | tests/test_replication.py | 32 | ||||
-rw-r--r-- | tests/testutils.py | 14 |
24 files changed, 444 insertions, 120 deletions
diff --git a/.travis.yml b/.travis.yml index 1aa2541..1041163 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,24 @@ +# Travis CI configuration file for psycopg2 + +dist: trusty +sudo: required language: python python: - - 2.6 - 2.7 - -before_script: - - psql -c 'create database psycopg2_test;' -U postgres + - 3.6-dev + - 2.6 + - 3.5 + - 3.4 + - 3.3 + - 3.2 install: - python setup.py install + - sudo scripts/travis_prepare.sh + +script: + - scripts/travis_test.sh -script: make check +notifications: + email: false @@ -24,6 +24,7 @@ New features: adapter is deprecated (:tickets:`#317, #343, #387`). - Added `~psycopg2.extensions.quote_ident()` function (:ticket:`#359`). - Added `~connection.get_dsn_parameters()` connection method (:ticket:`#364`). +- `~cursor.callproc()` now accepts a dictionary of parameters (:ticket:`#381`). Other changes: @@ -36,7 +37,7 @@ What's new in psycopg 2.6.3 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Throw an exception trying to pass ``NULL`` chars as parameters - (:ticket:`#420). + (:ticket:`#420`). - Make `~psycopg2.extras.Range` objects picklable (:ticket:`#462`). @@ -44,3 +44,8 @@ For any other resource (source code repository, bug tracker, mailing list) please check the `project homepage`__. .. __: http://initd.org/psycopg/ + + +.. image:: https://travis-ci.org/psycopg/psycopg2.svg?branch=master + :target: https://travis-ci.org/psycopg/psycopg2 + :alt: Build Status diff --git a/doc/src/conf.py b/doc/src/conf.py index 22c5c46..a918c08 100644 --- a/doc/src/conf.py +++ b/doc/src/conf.py @@ -61,8 +61,8 @@ except ImportError: release = version intersphinx_mapping = { - 'py': ('http://docs.python.org/', None), - 'py3': ('http://docs.python.org/3.4', None), + 'py': ('http://docs.python.org/2', None), + 'py3': ('http://docs.python.org/3', None), } # Pattern to generate links to the bug tracker diff --git a/doc/src/cursor.rst b/doc/src/cursor.rst index 45e6278..aee6b46 100644 --- a/doc/src/cursor.rst +++ b/doc/src/cursor.rst @@ -201,13 +201,19 @@ The ``cursor`` class Call a stored database procedure with the given name. The sequence of parameters must contain one entry for each argument that the procedure - expects. The result of the call is returned as modified copy of the - input sequence. Input parameters are left untouched, output and - 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. + expects. Overloaded procedures are supported. Named parameters can be + used by supplying the parameters as a dictionary. + + 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]) diff --git a/doc/src/faq.rst b/doc/src/faq.rst index d063666..89d8a63 100644 --- a/doc/src/faq.rst +++ b/doc/src/faq.rst @@ -241,7 +241,7 @@ How do I interrupt a long-running query in an interactive shell? .. code-block:: pycon - >>> psycopg2.extensions.set_wait_callback(psycopg2.extensions.wait_select) + >>> psycopg2.extensions.set_wait_callback(psycopg2.extras.wait_select) >>> cnn = psycopg2.connect('') >>> cur = cnn.cursor() >>> cur.execute("select pg_sleep(10)") diff --git a/lib/extras.py b/lib/extras.py index 5c4f5d2..7fc853a 100644 --- a/lib/extras.py +++ b/lib/extras.py @@ -908,7 +908,7 @@ WHERE typname = 'hstore'; def register_hstore(conn_or_curs, globally=False, unicode=False, 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 registered only on this object unless *globally* is set to `!True` diff --git a/psycopg/connection_int.c b/psycopg/connection_int.c index 62976d4..a63b47e 100644 --- a/psycopg/connection_int.c +++ b/psycopg/connection_int.c @@ -521,6 +521,25 @@ conn_setup_cancel(connectionObject *self, PGconn *pgconn) 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, 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; char *error = NULL; + int rv = -1; self->equote = conn_get_standard_conforming_strings(pgconn); self->server_version = conn_get_server_version(pgconn); self->protocol = conn_get_protocol_version(self->pgconn); if (3 != self->protocol) { PyErr_SetString(InterfaceError, "only protocol 3 supported"); - return -1; + goto exit; } if (0 > conn_read_encoding(self, pgconn)) { - return -1; + goto exit; } if (0 > conn_setup_cancel(self, pgconn)) { - return -1; + goto exit; } Py_BEGIN_ALLOW_THREADS; pthread_mutex_lock(&self->lock); Py_BLOCK_THREADS; - if (!conn_is_datestyle_ok(self->pgconn)) { + if (!dsn_has_replication(self->dsn) && !conn_is_datestyle_ok(self->pgconn)) { int res; Py_UNBLOCK_THREADS; res = pq_set_guc_locked(self, "datestyle", "ISO", @@ -578,18 +598,23 @@ conn_setup(connectionObject *self, PGconn *pgconn) Py_BLOCK_THREADS; if (res < 0) { pq_complete_error(self, &pgres, &error); - return -1; + goto unlock; } } /* for reset */ self->autocommit = 0; + /* success */ + rv = 0; + +unlock: Py_UNBLOCK_THREADS; pthread_mutex_unlock(&self->lock); Py_END_ALLOW_THREADS; - return 0; +exit: + return rv; } /* conn_connect - execute a connection to the database */ @@ -886,8 +911,11 @@ _conn_poll_setup_async(connectionObject *self) self->autocommit = 1; /* If the datestyle is ISO or anything else good, - * we can skip the CONN_STATUS_DATESTYLE step. */ - if (!conn_is_datestyle_ok(self->pgconn)) { + * we can skip the CONN_STATUS_DATESTYLE step. + * 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"); self->status = CONN_STATUS_DATESTYLE; if (0 == pq_send_query(self, psyco_datestyle)) { diff --git a/psycopg/cursor.h b/psycopg/cursor.h index e291d45..3c94fe3 100644 --- a/psycopg/cursor.h +++ b/psycopg/cursor.h @@ -80,6 +80,7 @@ struct cursorObject { char *qattr; /* quoting attr, used when quoting strings */ char *notice; /* a notice from the backend */ char *name; /* this cursor name */ + char *qname; /* this cursor name, quoted */ PyObject *string_types; /* a set of typecasters for string types */ PyObject *binary_types; /* a set of typecasters for binary types */ diff --git a/psycopg/cursor_type.c b/psycopg/cursor_type.c index fe79bbf..baa5b8f 100644 --- a/psycopg/cursor_type.c +++ b/psycopg/cursor_type.c @@ -55,7 +55,7 @@ psyco_curs_close(cursorObject *self) goto exit; } - if (self->name != NULL) { + if (self->qname != NULL) { char buffer[128]; PGTransactionStatusType status; @@ -68,7 +68,7 @@ psyco_curs_close(cursorObject *self) if (!(status == PQTRANS_UNKNOWN || status == PQTRANS_INERROR)) { 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; } else { @@ -422,10 +422,10 @@ _psyco_curs_execute(cursorObject *self, goto exit; } - if (self->name != NULL) { + if (self->qname != NULL) { self->query = Bytes_FromFormat( - "DECLARE \"%s\" %sCURSOR %s HOLD FOR %s", - self->name, + "DECLARE %s %sCURSOR %s HOLD FOR %s", + self->qname, scroll, self->withhold ? "WITH" : "WITHOUT", Bytes_AS_STRING(fquery)); @@ -436,10 +436,10 @@ _psyco_curs_execute(cursorObject *self, } } else { - if (self->name != NULL) { + if (self->qname != NULL) { self->query = Bytes_FromFormat( - "DECLARE \"%s\" %sCURSOR %s HOLD FOR %s", - self->name, + "DECLARE %s %sCURSOR %s HOLD FOR %s", + self->qname, scroll, self->withhold ? "WITH" : "WITHOUT", Bytes_AS_STRING(operation)); @@ -768,13 +768,13 @@ psyco_curs_fetchone(cursorObject *self) if (_psyco_curs_prefetch(self) < 0) return NULL; EXC_IF_NO_TUPLES(self); - if (self->name != NULL) { + if (self->qname != NULL) { char buffer[128]; EXC_IF_NO_MARK(self); EXC_IF_ASYNC_IN_PROGRESS(self, 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 (_psyco_curs_prefetch(self) < 0) return NULL; } @@ -823,8 +823,8 @@ psyco_curs_next_named(cursorObject *self) if (self->row >= self->rowcount) { char buffer[128]; - PyOS_snprintf(buffer, 127, "FETCH FORWARD %ld FROM \"%s\"", - self->itersize, self->name); + PyOS_snprintf(buffer, 127, "FETCH FORWARD %ld FROM %s", + self->itersize, self->qname); if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) 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; EXC_IF_NO_TUPLES(self); - if (self->name != NULL) { + if (self->qname != NULL) { char buffer[128]; EXC_IF_NO_MARK(self); EXC_IF_ASYNC_IN_PROGRESS(self, fetchmany); EXC_IF_TPC_PREPARED(self->conn, fetchone); - PyOS_snprintf(buffer, 127, "FETCH FORWARD %d FROM \"%s\"", - (int)size, self->name); + PyOS_snprintf(buffer, 127, "FETCH FORWARD %d FROM %s", + (int)size, self->qname); if (pq_execute(self, buffer, 0, 0, self->withhold) == -1) { 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; EXC_IF_NO_TUPLES(self); - if (self->name != NULL) { + if (self->qname != NULL) { char buffer[128]; EXC_IF_NO_MARK(self); EXC_IF_ASYNC_IN_PROGRESS(self, 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 (_psyco_curs_prefetch(self) < 0) { goto exit; } } @@ -1025,10 +1025,17 @@ psyco_curs_callproc(cursorObject *self, PyObject *args) PyObject *operation = NULL; PyObject *res = NULL; - if (!PyArg_ParseTuple(args, "s#|O", - &procname, &procname_len, ¶meters - )) - { goto exit; } + int using_dict; + PyObject *pname = NULL; + PyObject *pnames = NULL; + PyObject *pvals = NULL; + char *cpname = NULL; + char **scpnames = NULL; + + if (!PyArg_ParseTuple(args, "s#|O", &procname, &procname_len, + ¶meters)) { + goto exit; + } EXC_IF_CURS_CLOSED(self); EXC_IF_ASYNC_IN_PROGRESS(self, callproc); @@ -1036,7 +1043,7 @@ psyco_curs_callproc(cursorObject *self, PyObject *args) if (self->name != NULL) { psyco_set_error(ProgrammingError, self, - "can't call .callproc() on named cursors"); + "can't call .callproc() on named cursors"); goto exit; } @@ -1044,31 +1051,108 @@ psyco_curs_callproc(cursorObject *self, PyObject *args) if (-1 == (nparameters = PyObject_Length(parameters))) { goto exit; } } - /* allocate some memory, build the SQL and create a PyString from it */ - sl = procname_len + 17 + nparameters*3 - (nparameters ? 1 : 0); - sql = (char*)PyMem_Malloc(sl); - if (sql == NULL) { - PyErr_NoMemory(); - goto exit; + using_dict = nparameters > 0 && PyDict_Check(parameters); + + /* a Dict is complicated; the parameter names go into the query */ + if (using_dict) { + 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'; } - sprintf(sql, "SELECT * FROM %s(", procname); - for(i=0; i<nparameters; i++) { - strcat(sql, "%s,"); + /* 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'; } - sql[sl-2] = ')'; - sql[sl-1] = '\0'; - if (!(operation = Bytes_FromString(sql))) { goto exit; } + if (!(operation = Bytes_FromString(sql))) { + goto exit; + } - if (0 <= _psyco_curs_execute(self, operation, parameters, - self->conn->async, 0)) { - Py_INCREF(parameters); - res = parameters; + if (0 <= _psyco_curs_execute( + self, operation, pvals, self->conn->async, 0)) { + /* The dict case is outside DBAPI scope anyway, so simply return None */ + if (using_dict) { + res = Py_None; + } + else { + res = pvals; + } + Py_INCREF(res); } 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(pvals); PyMem_Free((void*)sql); 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 our own calculations to scroll; else we just delegate the scrolling to the MOVE SQL statement */ - if (self->name == NULL) { + if (self->qname == NULL) { if (strcmp(mode, "relative") == 0) { newpos = self->row + value; } 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); if (strcmp(mode, "absolute") == 0) { - PyOS_snprintf(buffer, 127, "MOVE ABSOLUTE %d FROM \"%s\"", - value, self->name); + PyOS_snprintf(buffer, 127, "MOVE ABSOLUTE %d FROM %s", + value, self->qname); } 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 (_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); 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; } } @@ -1891,6 +1978,7 @@ cursor_dealloc(PyObject* obj) cursor_clear(self); PyMem_Free(self->name); + PQfreemem(self->qname); CLEARPGRES(self->pgres); diff --git a/psycopg/psycopg.h b/psycopg/psycopg.h index 82b4293..438d763 100644 --- a/psycopg/psycopg.h +++ b/psycopg/psycopg.h @@ -128,7 +128,8 @@ RAISES HIDDEN PyObject *psyco_set_error(PyObject *exc, cursorObject *curs, const HIDDEN char *psycopg_escape_string(connectionObject *conn, 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_is_text_file(PyObject *f); diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index 012df6b..bf7d908 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -62,7 +62,6 @@ HIDDEN PyObject *pyDateTimeModuleP = NULL; HIDDEN PyObject *psycoEncodings = NULL; - #ifdef PSYCOPG_DEBUG HIDDEN int psycopg_debug_enabled = 0; #endif @@ -191,9 +190,8 @@ psyco_quote_ident(PyObject *self, PyObject *args, PyObject *kwargs) str = Bytes_AS_STRING(ident); - quoted = PQescapeIdentifier(conn->pgconn, str, strlen(str)); + quoted = psycopg_escape_identifier(conn, str, strlen(str)); if (!quoted) { - PyErr_NoMemory(); goto exit; } result = conn_text_from_chars(conn, quoted); diff --git a/psycopg/utils.c b/psycopg/utils.c index 631b839..bc6f7be 100644 --- a/psycopg/utils.c +++ b/psycopg/utils.c @@ -90,43 +90,40 @@ psycopg_escape_string(connectionObject *conn, const char *from, Py_ssize_t len, 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. * - * The returned string doesn't include quotes. - * - * 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. + * Return a string allocated by Postgres: free it using PQfreemem + * In case of error set a Python exception. */ 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; - const char *src; - char *dst; + char *rv = NULL; - if (!len) { len = strlen(from); } - if (!(rv = PyMem_New(char, 1 + 2 * len))) { - PyErr_NoMemory(); - return NULL; + if (!conn || !conn->pgconn) { + PyErr_SetString(InterfaceError, "connection not valid"); + goto exit; } - /* The only thing to do is double quotes */ - for (src = from, dst = rv; *src; ++src, ++dst) { - *dst = *src; - if ('"' == *src) { - *++dst = '"'; + if (!len) { len = strlen(str); } + + rv = PQescapeIdentifier(conn->pgconn, str, len); + if (!rv) { + 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; } + /* Duplicate a string. * * Allocate a new buffer on the Python heap containing the new string. diff --git a/scripts/travis_prepare.sh b/scripts/travis_prepare.sh new file mode 100755 index 0000000..2b1e12e --- /dev/null +++ b/scripts/travis_prepare.sh @@ -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 diff --git a/scripts/travis_test.sh b/scripts/travis_test.sh new file mode 100755 index 0000000..0c60b93 --- /dev/null +++ b/scripts/travis_test.sh @@ -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 @@ -7,24 +7,23 @@ define= # "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 -# different name uncomment the following option and set it to the pg_config -# full path. -#pg_config= +# different name set the following option to the pg_config full path. +pg_config= # Set to 1 to use Python datetime objects for default date/time representation. use_pydatetime=1 # If the build system does not find the mx.DateTime headers, try -# uncommenting the following line and setting its value to the right path. -#mx_include_dir= +# setting its value to the right path. +mx_include_dir= # For Windows only: # Set to 1 if the PostgreSQL library was built with OpenSSL. # Required to link in OpenSSL libraries and dependencies. have_ssl=0 -# Statically link against the postgresql client library. -#static_libpq=1 +# Set to 1 to statically link against the postgresql client library. +static_libpq=0 # Add here eventual extra libraries required to link the module. -#libraries= +libraries= @@ -381,6 +381,11 @@ class psycopg_build_ext(build_ext): def finalize_options(self): """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) 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') else: 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 include_dirs.append(mxincludedir) define_macros.append(('HAVE_MXDATETIME', '1')) diff --git a/tests/test_connection.py b/tests/test_connection.py index 8744488..833751b 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -465,7 +465,7 @@ class MakeDsnTestCase(ConnectingTestCase): conn = self.connect() d = conn.get_dsn_parameters() 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): diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 4aae6b2..fc924c4 100755 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -498,6 +498,48 @@ class CursorTests(ConnectingTestCase): cur = self.conn.cursor() 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(): return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/tests/test_module.py b/tests/test_module.py index 1a9a19d..6a1606d 100755 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -119,8 +119,8 @@ class ConnectTestCase(unittest.TestCase): def test_int_port_param(self): psycopg2.connect(database='sony', port=6543) dsn = " %s " % self.args[0] - self.assertIn(" dbname=sony ", dsn) - self.assertIn(" port=6543 ", dsn) + self.assert_(" dbname=sony " in dsn, dsn) + self.assert_(" port=6543 " in dsn, dsn) def test_empty_param(self): psycopg2.connect(database='sony', password='') diff --git a/tests/test_psycopg2_dbapi20.py b/tests/test_psycopg2_dbapi20.py index 80473b7..c780d50 100755 --- a/tests/test_psycopg2_dbapi20.py +++ b/tests/test_psycopg2_dbapi20.py @@ -36,7 +36,31 @@ class Psycopg2Tests(dbapi20.DatabaseAPI20Test): connect_args = () 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): # psycopg2's setoutputsize() is a no-op diff --git a/tests/test_quote.py b/tests/test_quote.py index f74fd85..72c9c1e 100755 --- a/tests/test_quote.py +++ b/tests/test_quote.py @@ -65,11 +65,13 @@ class QuotingTestCase(ConnectingTestCase): curs = self.conn.cursor() data = 'abcd\x01\x00cdefg' - with self.assertRaises(ValueError) as e: + try: curs.execute("SELECT %s", (data,)) - - self.assertEquals(str(e.exception), - 'A string literal cannot contain NUL (0x00) characters.') + except ValueError as e: + self.assertEquals(str(e), + 'A string literal cannot contain NUL (0x00) characters.') + else: + self.fail("ValueError not raised") def test_binary(self): data = b"""some data with \000\013 binary diff --git a/tests/test_replication.py b/tests/test_replication.py index ca99038..79d1295 100644..100755 --- a/tests/test_replication.py +++ b/tests/test_replication.py @@ -23,23 +23,19 @@ # License for more details. import psycopg2 -import psycopg2.extensions from psycopg2.extras import ( PhysicalReplicationConnection, LogicalReplicationConnection, StopReplication) import testconfig -from testutils import unittest -from testutils import skip_before_postgres -from testutils import ConnectingTestCase +from testutils import unittest, ConnectingTestCase +from testutils import skip_before_postgres, skip_if_green + +skip_repl_if_green = skip_if_green("replication not supported in green mode") class ReplicationTestCase(ConnectingTestCase): def setUp(self): - if not testconfig.repl_dsn: - self.skipTest("replication tests disabled by default") - super(ReplicationTestCase, self).setUp() - self.slot = testconfig.repl_slot self._slots = [] @@ -93,6 +89,20 @@ class ReplicationTest(ReplicationTestCase): cur.execute("IDENTIFY_SYSTEM") 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) def test_logical_replication_connection(self): conn = self.repl_connect(connection_factory=LogicalReplicationConnection) @@ -114,6 +124,7 @@ class ReplicationTest(ReplicationTestCase): psycopg2.ProgrammingError, self.create_replication_slot, cur) @skip_before_postgres(9, 4) # slots require 9.4 + @skip_repl_if_green def test_start_on_missing_replication_slot(self): conn = self.repl_connect(connection_factory=PhysicalReplicationConnection) if conn is None: @@ -127,6 +138,7 @@ class ReplicationTest(ReplicationTestCase): cur.start_replication(self.slot) @skip_before_postgres(9, 4) # slots require 9.4 + @skip_repl_if_green def test_start_and_recover_from_error(self): conn = self.repl_connect(connection_factory=LogicalReplicationConnection) if conn is None: @@ -148,6 +160,7 @@ class ReplicationTest(ReplicationTestCase): cur.start_replication(slot_name=self.slot) @skip_before_postgres(9, 4) # slots require 9.4 + @skip_repl_if_green def test_stop_replication(self): conn = self.repl_connect(connection_factory=LogicalReplicationConnection) if conn is None: @@ -167,12 +180,13 @@ class ReplicationTest(ReplicationTestCase): class AsyncReplicationTest(ReplicationTestCase): @skip_before_postgres(9, 4) # slots require 9.4 + @skip_repl_if_green def test_async_replication(self): conn = self.repl_connect( connection_factory=LogicalReplicationConnection, async=1) if conn is None: return - self.wait(conn) + cur = conn.cursor() self.create_replication_slot(cur, output_plugin='test_decoding') diff --git a/tests/testutils.py b/tests/testutils.py index d0a34bc..9347735 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -122,13 +122,25 @@ class ConnectingTestCase(unittest.TestCase): Should raise a skip test if not available, but guard for None on old Python versions. """ + if repl_dsn is None: + return self.skipTest("replication tests disabled by default") + if 'dsn' not in kwargs: kwargs['dsn'] = repl_dsn import psycopg2 try: conn = self.connect(**kwargs) + if conn.async == 1: + self.wait(conn) 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 def _get_conn(self): |