summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>2016-12-26 12:06:21 +0100
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>2016-12-26 12:06:21 +0100
commit7caba160b7083c64197329e17d0d0e0eb17c8639 (patch)
tree9c7a8221ccdbbf3efdbac0ccd49287cc90647a6f
parent121cf3b8f8426765d983579d3a4b2e932429cd9f (diff)
parente9577e9b890fd9a27bb146e8ea1c24eb562f28b2 (diff)
downloadpsycopg2-7caba160b7083c64197329e17d0d0e0eb17c8639.tar.gz
Merge branch 'master' into fast-codecs
-rw-r--r--.travis.yml21
-rw-r--r--NEWS3
-rw-r--r--README.rst5
-rw-r--r--doc/src/conf.py4
-rw-r--r--doc/src/cursor.rst18
-rw-r--r--doc/src/faq.rst2
-rw-r--r--lib/extras.py2
-rw-r--r--psycopg/connection_int.c44
-rw-r--r--psycopg/cursor.h1
-rw-r--r--psycopg/cursor_type.c174
-rw-r--r--psycopg/psycopg.h3
-rw-r--r--psycopg/psycopgmodule.c4
-rw-r--r--psycopg/utils.c41
-rwxr-xr-xscripts/travis_prepare.sh60
-rwxr-xr-xscripts/travis_test.sh30
-rw-r--r--setup.cfg15
-rw-r--r--setup.py7
-rwxr-xr-xtests/test_connection.py2
-rwxr-xr-xtests/test_cursor.py42
-rwxr-xr-xtests/test_module.py4
-rwxr-xr-xtests/test_psycopg2_dbapi20.py26
-rwxr-xr-xtests/test_quote.py10
-rwxr-xr-x[-rw-r--r--]tests/test_replication.py32
-rw-r--r--tests/testutils.py14
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
diff --git a/NEWS b/NEWS
index b7efef2..6ffa66a 100644
--- a/NEWS
+++ b/NEWS
@@ -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`).
diff --git a/README.rst b/README.rst
index 51d2d6b..f18be56 100644
--- a/README.rst
+++ b/README.rst
@@ -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, &parameters
- ))
- { 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,
+ &parameters)) {
+ 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
diff --git a/setup.cfg b/setup.cfg
index 90a47dd..0d41934 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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=
diff --git a/setup.py b/setup.py
index 3f02183..c106525 100644
--- a/setup.py
+++ b/setup.py
@@ -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):