diff --git a/NEWS b/NEWS index 42943a45..b87c1101 100644 --- a/NEWS +++ b/NEWS @@ -35,6 +35,10 @@ New features: (:ticket:`#491`). - Added ``async_`` as an alias for ``async`` to support Python 3.7 where ``async`` will become a keyword (:ticket:`#495`). +- Unless in autocommit, do not use :sql:`default_transaction_*` settings to + control the session characteristics as it may create problems with external + connection pools such as pgbouncer; use :sql:`BEGIN` options instead + (:ticket:`#503`). Bug fixes: @@ -46,6 +50,11 @@ Other changes: - Dropped support for Python 2.5 and 3.1. - Dropped support for client library older than PostgreSQL 9.1 (but older server versions are still supported). +- `~connection.isolation_level` doesn't read from the database but will return + `~psycopg2.extensions.ISOLATION_LEVEL_DEFAULT` if no value was set on the + connection. +- `~connection.set_isolation_level()` will throw an exception if executed + inside a transaction; previously it would have silently rolled it back. What's new in psycopg 2.6.3 diff --git a/doc/src/connection.rst b/doc/src/connection.rst index 0bc584ca..2adad597 100644 --- a/doc/src/connection.rst +++ b/doc/src/connection.rst @@ -400,6 +400,32 @@ The ``connection`` class .. versionadded:: 2.4.2 + .. versionchanged:: 2.7 + Before this version, the function would have set + :sql:`default_transaction_*` attribute in the current session; + this implementation has the problem of not playing well with + external connection pooling working at transaction level and not + resetting the state of the session: changing the default + transaction would pollute the connections in the pool and create + problems to other applications using the same pool. + + Starting from 2.7, if the connection is not autocommit, the + transaction characteristics are issued together with :sql:`BEGIN` + and will leave the :sql:`default_transaction_*` settings untouched. + For example:: + + conn.set_session(readonly=True) + + will not change :sql:`default_transaction_read_only`, but + following transaction will start with a :sql:`BEGIN READ ONLY`. + Conversely, using:: + + conn.set_session(readonly=True, autocommit=True) + + will set :sql:`default_transaction_read_only` to :sql:`on` and + rely on the server to apply the read only state to whatever + transaction, implicit or explicit, is executed in the connection. + .. attribute:: autocommit @@ -428,32 +454,54 @@ The ``connection`` class .. versionadded:: 2.4.2 - .. attribute:: isolation_level .. method:: set_isolation_level(level) .. note:: - From version 2.4.2, `set_session()` and `autocommit`, offer + From version 2.4.2, `set_session()` and `autocommit` offer finer control on the transaction characteristics. - Read or set the `transaction isolation level`_ for the current session. + Set the `transaction isolation level`_ for the current session. The level defines the different phenomena that can happen in the database between concurrent transactions. - The value set or read is an integer: symbolic constants are defined in + The value set is an integer: symbolic constants are defined in the module `psycopg2.extensions`: see :ref:`isolation-level-constants` for the available values. - The default level is :sql:`READ COMMITTED`: at this level a - transaction is automatically started the first time a database command - is executed. If you want an *autocommit* mode, switch to - `~psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT` before + The default level is `~psycopg2.extensions.ISOLATION_LEVEL_DEFAULT`: + at this level a transaction is automatically started the first time a + database command is executed. If you want an *autocommit* mode, + switch to `~psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT` before executing any command:: >>> conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) See also :ref:`transactions-control`. + .. versionchanged:: 2.7 + + the function must be called outside a transaction; previously it + would have executed an implicit :sql:`ROLLBACK`; it will now raise + an exception. + + + .. attribute:: isolation_level + + Read the `transaction isolation level`_ for the current session. The + value is one of the :ref:`isolation-level-constants` defined in the + `psycopg2.extensions` module. + + .. versionchanged:: 2.7 + + the default value for `!isolation_level` is + `~psycopg2.extensions.ISOLATION_LEVEL_DEFAULT`; previously the + property would have queried the server and returned the real value + applied. To know this value you can run a query such as :sql:`show + transaction_isolation`. Usually the default value is `READ + COMMITTED`, but this may be changed in the server configuration. + + .. index:: pair: Client; Encoding diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index 8d70ba38..ae40b720 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -567,15 +567,16 @@ Isolation level constants ------------------------- Psycopg2 `connection` objects hold informations about the PostgreSQL -`transaction isolation level`_. The current transaction level can be read -from the `~connection.isolation_level` attribute. The default isolation -level is :sql:`READ COMMITTED`. A different isolation level con be set -through the `~connection.set_isolation_level()` method. The level can be -set to one of the following constants: +`transaction isolation level`_. By default Psycopg doesn't change the default +configuration of the server (`ISOLATION_LEVEL_DEFAULT`); the default for +PostgreSQL servers is typically :sql:`READ COMMITTED`, but this may be changed +in the server configuration files. A different isolation level can be set +through the `~connection.set_isolation_level()` or `~connection.set_session()` +methods. The level can be set to one of the following constants: .. data:: ISOLATION_LEVEL_AUTOCOMMIT - No transaction is started when command are issued and no + No transaction is started when commands are executed and no `~connection.commit()` or `~connection.rollback()` is required. Some PostgreSQL command such as :sql:`CREATE DATABASE` or :sql:`VACUUM` can't run into a transaction: to run such command use:: @@ -651,6 +652,16 @@ set to one of the following constants: .. __: http://www.postgresql.org/docs/current/static/transaction-iso.html#XACT-SERIALIZABLE +.. data:: ISOLATION_LEVEL_DEFAULT + + A new transaction is started at the first `~cursor.execute()` command, but + the isolation level is not explicitly selected by Psycopg: the server will + use whatever level is defined in its configuration or by statements + executed within the session outside Pyscopg control. If you want to know + what the value is you can use a query such as :sql:`show + transaction_isolation`. + + .. versionadded:: 2.7 .. index:: diff --git a/doc/src/usage.rst b/doc/src/usage.rst index 1366485a..6cb038b6 100644 --- a/doc/src/usage.rst +++ b/doc/src/usage.rst @@ -676,8 +676,7 @@ commands executed will be immediately committed and no rollback is possible. A few commands (e.g. :sql:`CREATE DATABASE`, :sql:`VACUUM`...) require to be run outside any transaction: in order to be able to run these commands from Psycopg, the connection must be in autocommit mode: you can use the -`~connection.autocommit` property (`~connection.set_isolation_level()` in -older versions). +`~connection.autocommit` property. .. warning:: diff --git a/lib/extensions.py b/lib/extensions.py index b123e881..f4dc706f 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -72,6 +72,7 @@ ISOLATION_LEVEL_READ_UNCOMMITTED = 4 ISOLATION_LEVEL_READ_COMMITTED = 1 ISOLATION_LEVEL_REPEATABLE_READ = 2 ISOLATION_LEVEL_SERIALIZABLE = 3 +ISOLATION_LEVEL_DEFAULT = 5 """psycopg connection status values.""" diff --git a/psycopg/connection.h b/psycopg/connection.h index 2e2d51de..65efcaf0 100644 --- a/psycopg/connection.h +++ b/psycopg/connection.h @@ -38,6 +38,12 @@ extern "C" { #define ISOLATION_LEVEL_READ_COMMITTED 1 #define ISOLATION_LEVEL_REPEATABLE_READ 2 #define ISOLATION_LEVEL_SERIALIZABLE 3 +#define ISOLATION_LEVEL_DEFAULT 5 + +/* 3-state values on/off/default */ +#define STATE_OFF 0 +#define STATE_ON 1 +#define STATE_DEFAULT 2 /* connection status */ #define CONN_STATUS_SETUP 0 @@ -129,6 +135,11 @@ struct connectionObject { * codecs.getdecoder('utf8') */ PyObject *pyencoder; /* python codec encoding function */ PyObject *pydecoder; /* python codec decoding function */ + + /* Values for the transactions characteristics */ + int isolevel; + int readonly; + int deferrable; }; /* map isolation level values into a numeric const */ @@ -155,11 +166,8 @@ HIDDEN void conn_close(connectionObject *self); HIDDEN void conn_close_locked(connectionObject *self); RAISES_NEG HIDDEN int conn_commit(connectionObject *self); RAISES_NEG HIDDEN int conn_rollback(connectionObject *self); -RAISES_NEG HIDDEN int conn_set_session(connectionObject *self, const char *isolevel, - const char *readonly, const char *deferrable, - int autocommit); -HIDDEN int conn_set_autocommit(connectionObject *self, int value); -RAISES_NEG HIDDEN int conn_switch_isolation_level(connectionObject *self, int level); +RAISES_NEG HIDDEN int conn_set_session(connectionObject *self, int autocommit, + int isolevel, int readonly, int deferrable); RAISES_NEG HIDDEN int conn_set_client_encoding(connectionObject *self, const char *enc); HIDDEN int conn_poll(connectionObject *self); RAISES_NEG HIDDEN int conn_tpc_begin(connectionObject *self, xidObject *xid); diff --git a/psycopg/connection_int.c b/psycopg/connection_int.c index f92a658e..e6906eb5 100644 --- a/psycopg/connection_int.c +++ b/psycopg/connection_int.c @@ -34,18 +34,36 @@ #include -/* Mapping from isolation level name to value exposed by Python. - * - * Note: ordering matters: to get a valid pre-PG 8 level from one not valid, - * we increase a pointer in this list by one position. */ -const IsolationLevel conn_isolevels[] = { - {"", ISOLATION_LEVEL_AUTOCOMMIT}, - {"read uncommitted", ISOLATION_LEVEL_READ_UNCOMMITTED}, - {"read committed", ISOLATION_LEVEL_READ_COMMITTED}, - {"repeatable read", ISOLATION_LEVEL_REPEATABLE_READ}, - {"serializable", ISOLATION_LEVEL_SERIALIZABLE}, - {"default", -1}, /* never to be found on the server */ - { NULL } +/* String indexes match the ISOLATION_LEVEL_* consts */ +const char *srv_isolevels[] = { + NULL, /* autocommit */ + "READ COMMITTED", + "REPEATABLE READ", + "SERIALIZABLE", + "READ UNCOMMITTED", + "default" /* only to set GUC, not for BEGIN */ +}; + +/* Read only false, true */ +const char *srv_readonly[] = { + " READ WRITE", + " READ ONLY", + "" /* default */ +}; + +/* Deferrable false, true */ +const char *srv_deferrable[] = { + " NOT DEFERRABLE", + " DEFERRABLE", + "" /* default */ +}; + +/* On/Off/Default GUC states + */ +const char *srv_state_guc[] = { + "off", + "on", + "default" }; @@ -553,50 +571,13 @@ exit: RAISES_NEG int conn_get_isolation_level(connectionObject *self) { - PGresult *pgres = NULL; - char *error = NULL; - int rv = -1; - char *lname; - const IsolationLevel *level; - /* this may get called by async connections too: here's your result */ if (self->autocommit) { - return 0; + return ISOLATION_LEVEL_AUTOCOMMIT; } - - Py_BEGIN_ALLOW_THREADS; - pthread_mutex_lock(&self->lock); - - if (!(lname = pq_get_guc_locked(self, "default_transaction_isolation", - &pgres, &error, &_save))) { - goto endlock; + else { + return self->isolevel; } - - /* find the value for the requested isolation level */ - level = conn_isolevels; - while ((++level)->name) { - if (0 == strcasecmp(level->name, lname)) { - rv = level->value; - break; - } - } - if (-1 == rv) { - error = malloc(256); - PyOS_snprintf(error, 256, - "unexpected isolation level: '%s'", lname); - } - - free(lname); - -endlock: - pthread_mutex_unlock(&self->lock); - Py_END_ALLOW_THREADS; - - if (rv < 0) { - pq_complete_error(self, &pgres, &error); - } - - return rv; } @@ -1208,156 +1189,98 @@ conn_rollback(connectionObject *self) return res; } + +/* Change the state of the session */ RAISES_NEG int -conn_set_session(connectionObject *self, - const char *isolevel, const char *readonly, const char *deferrable, - int autocommit) +conn_set_session(connectionObject *self, int autocommit, + int isolevel, int readonly, int deferrable) { + int rv = -1; PGresult *pgres = NULL; char *error = NULL; - int res = -1; - Py_BEGIN_ALLOW_THREADS; - pthread_mutex_lock(&self->lock); - - if (isolevel) { - Dprintf("conn_set_session: setting isolation to %s", isolevel); - if ((res = pq_set_guc_locked(self, - "default_transaction_isolation", isolevel, - &pgres, &error, &_save))) { - goto endlock; - } - } - - if (readonly) { - Dprintf("conn_set_session: setting read only to %s", readonly); - if ((res = pq_set_guc_locked(self, - "default_transaction_read_only", readonly, - &pgres, &error, &_save))) { - goto endlock; - } - } - - if (deferrable) { - Dprintf("conn_set_session: setting deferrable to %s", deferrable); - if ((res = pq_set_guc_locked(self, - "default_transaction_deferrable", deferrable, - &pgres, &error, &_save))) { - goto endlock; - } - } - - if (self->autocommit != autocommit) { - Dprintf("conn_set_session: setting autocommit to %d", autocommit); - self->autocommit = autocommit; - } - - res = 0; - -endlock: - pthread_mutex_unlock(&self->lock); - Py_END_ALLOW_THREADS; - - if (res < 0) { - pq_complete_error(self, &pgres, &error); - } - - return res; -} - -int -conn_set_autocommit(connectionObject *self, int value) -{ - Py_BEGIN_ALLOW_THREADS; - pthread_mutex_lock(&self->lock); - - self->autocommit = value; - - pthread_mutex_unlock(&self->lock); - Py_END_ALLOW_THREADS; - - return 0; -} - -/* conn_switch_isolation_level - switch isolation level on the connection */ - -RAISES_NEG int -conn_switch_isolation_level(connectionObject *self, int level) -{ - PGresult *pgres = NULL; - char *error = NULL; - int curr_level; - int ret = -1; - - /* use only supported levels on older PG versions */ + /* Promote an isolation level to one of the levels supported by the server */ if (self->server_version < 80000) { - if (level == ISOLATION_LEVEL_READ_UNCOMMITTED) - level = ISOLATION_LEVEL_READ_COMMITTED; - else if (level == ISOLATION_LEVEL_REPEATABLE_READ) - level = ISOLATION_LEVEL_SERIALIZABLE; + if (isolevel == ISOLATION_LEVEL_READ_UNCOMMITTED) { + isolevel = ISOLATION_LEVEL_READ_COMMITTED; + } + else if (isolevel == ISOLATION_LEVEL_REPEATABLE_READ) { + isolevel = ISOLATION_LEVEL_SERIALIZABLE; + } } - if (-1 == (curr_level = conn_get_isolation_level(self))) { - return -1; - } - - if (curr_level == level) { - /* no need to change level */ - return 0; - } - - /* Emulate the previous semantic of set_isolation_level() using the - * functions currently available. */ - Py_BEGIN_ALLOW_THREADS; pthread_mutex_lock(&self->lock); - /* terminate the current transaction if any */ - if ((ret = pq_abort_locked(self, &pgres, &error, &_save))) { - goto endlock; - } - - if (level == 0) { - if ((ret = pq_set_guc_locked(self, - "default_transaction_isolation", "default", - &pgres, &error, &_save))) { - goto endlock; - } - self->autocommit = 1; - } - else { - /* find the name of the requested level */ - const IsolationLevel *isolevel = conn_isolevels; - while ((++isolevel)->name) { - if (level == isolevel->value) { - break; + if (autocommit) { + /* we are in autocommit state, so no BEGIN will be issued: + * configure the session with the characteristics requested */ + if (isolevel != self->isolevel) { + if (0 > pq_set_guc_locked(self, + "default_transaction_isolation", srv_isolevels[isolevel], + &pgres, &error, &_save)) { + goto endlock; } } - if (!isolevel->name) { - ret = -1; - error = strdup("bad isolation level value"); + if (readonly != self->readonly) { + if (0 > pq_set_guc_locked(self, + "default_transaction_read_only", srv_state_guc[readonly], + &pgres, &error, &_save)) { + goto endlock; + } + } + if (deferrable != self->deferrable && self->server_version >= 90100) { + if (0 > pq_set_guc_locked(self, + "default_transaction_deferrable", srv_state_guc[deferrable], + &pgres, &error, &_save)) { + goto endlock; + } + } + } + else if (self->autocommit) { + /* we are moving from autocommit to not autocommit, so revert the + * characteristics to defaults to let BEGIN do its work */ + if (0 > pq_set_guc_locked(self, + "default_transaction_isolation", "default", + &pgres, &error, &_save)) { goto endlock; } - - if ((ret = pq_set_guc_locked(self, - "default_transaction_isolation", isolevel->name, - &pgres, &error, &_save))) { + if (0 > pq_set_guc_locked(self, + "default_transaction_read_only", "default", + &pgres, &error, &_save)) { goto endlock; } - self->autocommit = 0; + if (self->server_version >= 90100) { + if (0 > pq_set_guc_locked(self, + "default_transaction_deferrable", "default", + &pgres, &error, &_save)) { + goto endlock; + } + } } - Dprintf("conn_switch_isolation_level: switched to level %d", level); + self->autocommit = autocommit; + self->isolevel = isolevel; + self->readonly = readonly; + self->deferrable = deferrable; + rv = 0; endlock: pthread_mutex_unlock(&self->lock); Py_END_ALLOW_THREADS; - if (ret < 0) { + if (rv < 0) { pq_complete_error(self, &pgres, &error); + goto exit; } - return ret; + Dprintf( + "conn_set_session: autocommit %d, isolevel %d, readonly %d, deferrable %d", + autocommit, isolevel, readonly, deferrable); + + +exit: + return rv; } diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index 2066579e..7647056b 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -36,6 +36,9 @@ #include #include +extern HIDDEN const char *srv_isolevels[]; +extern HIDDEN const char *srv_readonly[]; +extern HIDDEN const char *srv_deferrable[]; /** DBAPI methods **/ @@ -444,18 +447,17 @@ exit: /* parse a python object into one of the possible isolation level values */ -extern const IsolationLevel conn_isolevels[]; - -static const char * -_psyco_conn_parse_isolevel(connectionObject *self, PyObject *pyval) +RAISES_NEG static int +_psyco_conn_parse_isolevel(PyObject *pyval) { - const IsolationLevel *isolevel = NULL; + int rv = -1; + long level; Py_INCREF(pyval); /* for ensure_bytes */ /* parse from one of the level constants */ if (PyInt_Check(pyval)) { - long level = PyInt_AsLong(pyval); + level = PyInt_AsLong(pyval); if (level == -1 && PyErr_Occurred()) { goto exit; } if (level < 1 || level > 4) { PyErr_SetString(PyExc_ValueError, @@ -463,65 +465,80 @@ _psyco_conn_parse_isolevel(connectionObject *self, PyObject *pyval) goto exit; } - isolevel = conn_isolevels; - while ((++isolevel)->value != level) - ; /* continue */ + rv = level; } /* parse from the string -- this includes "default" */ + else { - isolevel = conn_isolevels; - while ((++isolevel)->name) { - if (!(pyval = psycopg_ensure_bytes(pyval))) { - goto exit; - } - if (0 == strcasecmp(isolevel->name, Bytes_AS_STRING(pyval))) { + if (!(pyval = psycopg_ensure_bytes(pyval))) { + goto exit; + } + for (level = 1; level <= 4; level++) { + if (0 == strcasecmp(srv_isolevels[level], Bytes_AS_STRING(pyval))) { + rv = level; break; } } - if (!isolevel->name) { - char msg[256]; - snprintf(msg, sizeof(msg), - "bad value for isolation_level: '%s'", Bytes_AS_STRING(pyval)); - PyErr_SetString(PyExc_ValueError, msg); + if (rv < 0 && 0 == strcasecmp("default", Bytes_AS_STRING(pyval))) { + rv = ISOLATION_LEVEL_DEFAULT; } - } - - /* use only supported levels on older PG versions */ - if (isolevel && self->server_version < 80000) { - if (isolevel->value == ISOLATION_LEVEL_READ_UNCOMMITTED - || isolevel->value == ISOLATION_LEVEL_REPEATABLE_READ) { - ++isolevel; + if (rv < 0) { + PyErr_Format(PyExc_ValueError, + "bad value for isolation_level: '%s'", Bytes_AS_STRING(pyval)); + goto exit; } } exit: Py_XDECREF(pyval); - return isolevel ? isolevel->name : NULL; + return rv; } -/* convert True/False/"default" into a C string */ +/* convert False/True/"default" -> 0/1/2 */ -static const char * +RAISES_NEG static int _psyco_conn_parse_onoff(PyObject *pyval) { - int istrue = PyObject_IsTrue(pyval); - if (-1 == istrue) { return NULL; } - if (istrue) { - int cmp; - PyObject *pydef; - if (!(pydef = Text_FromUTF8("default"))) { return NULL; } - cmp = PyObject_RichCompareBool(pyval, pydef, Py_EQ); - Py_DECREF(pydef); - if (-1 == cmp) { return NULL; } - return cmp ? "default" : "on"; + int rv = -1; + + Py_INCREF(pyval); /* for ensure_bytes */ + + if (PyUnicode_CheckExact(pyval) || Bytes_CheckExact(pyval)) { + if (!(pyval = psycopg_ensure_bytes(pyval))) { + goto exit; + } + if (0 == strcasecmp("default", Bytes_AS_STRING(pyval))) { + rv = STATE_DEFAULT; + } + else { + PyErr_Format(PyExc_ValueError, + "the only string accepted is 'default'; got %s", + Bytes_AS_STRING(pyval)); + goto exit; + } } else { - return "off"; + int istrue; + if (0 > (istrue = PyObject_IsTrue(pyval))) { goto exit; } + rv = istrue ? STATE_ON : STATE_OFF; } + +exit: + Py_XDECREF(pyval); + + return rv; } +#define _set_session_checks(self,what) \ +do { \ + EXC_IF_CONN_CLOSED(self); \ + EXC_IF_CONN_ASYNC(self, what); \ + EXC_IF_IN_TRANSACTION(self, what); \ + EXC_IF_TPC_PREPARED(self, what); \ +} while(0) + /* set_session - set default transaction characteristics */ #define psyco_conn_set_session_doc \ @@ -536,17 +553,15 @@ psyco_conn_set_session(connectionObject *self, PyObject *args, PyObject *kwargs) PyObject *deferrable = Py_None; PyObject *autocommit = Py_None; - const char *c_isolevel = NULL; - const char *c_readonly = NULL; - const char *c_deferrable = NULL; + int c_isolevel = self->isolevel; + int c_readonly = self->readonly; + int c_deferrable = self->deferrable; int c_autocommit = self->autocommit; static char *kwlist[] = {"isolation_level", "readonly", "deferrable", "autocommit", NULL}; - EXC_IF_CONN_CLOSED(self); - EXC_IF_CONN_ASYNC(self, set_session); - EXC_IF_IN_TRANSACTION(self, set_session); + _set_session_checks(self, set_session); if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OOOO", kwlist, &isolevel, &readonly, &deferrable, &autocommit)) { @@ -554,13 +569,13 @@ psyco_conn_set_session(connectionObject *self, PyObject *args, PyObject *kwargs) } if (Py_None != isolevel) { - if (!(c_isolevel = _psyco_conn_parse_isolevel(self, isolevel))) { + if (0 > (c_isolevel = _psyco_conn_parse_isolevel(isolevel))) { return NULL; } } if (Py_None != readonly) { - if (!(c_readonly = _psyco_conn_parse_onoff(readonly))) { + if (0 > (c_readonly = _psyco_conn_parse_onoff(readonly))) { return NULL; } } @@ -571,17 +586,17 @@ psyco_conn_set_session(connectionObject *self, PyObject *args, PyObject *kwargs) " from PostgreSQL 9.1"); return NULL; } - if (!(c_deferrable = _psyco_conn_parse_onoff(deferrable))) { + if (0 > (c_deferrable = _psyco_conn_parse_onoff(readonly))) { return NULL; } } + if (Py_None != autocommit) { - c_autocommit = PyObject_IsTrue(autocommit); - if (-1 == c_autocommit) { return NULL; } + if (-1 == (c_autocommit = PyObject_IsTrue(autocommit))) { return NULL; } } - if (0 > conn_set_session(self, - c_isolevel, c_readonly, c_deferrable, c_autocommit)) { + if (0 > conn_set_session( + self, c_autocommit, c_isolevel, c_readonly, c_deferrable)) { return NULL; } @@ -606,9 +621,7 @@ _psyco_conn_autocommit_set_checks(connectionObject *self) { /* wrapper to use the EXC_IF macros. * return NULL in case of error, else whatever */ - EXC_IF_CONN_CLOSED(self); - EXC_IF_CONN_ASYNC(self, autocommit); - EXC_IF_IN_TRANSACTION(self, autocommit); + _set_session_checks(self, autocommit); return Py_None; /* borrowed */ } @@ -619,7 +632,10 @@ psyco_conn_autocommit_set(connectionObject *self, PyObject *pyvalue) if (!_psyco_conn_autocommit_set_checks(self)) { return -1; } if (-1 == (value = PyObject_IsTrue(pyvalue))) { return -1; } - if (0 != conn_set_autocommit(self, value)) { return -1; } + if (0 > conn_set_session(self, value, + self->isolevel, self->readonly, self->deferrable)) { + return -1; + } return 0; } @@ -651,20 +667,27 @@ psyco_conn_set_isolation_level(connectionObject *self, PyObject *args) { int level = 1; - EXC_IF_CONN_CLOSED(self); - EXC_IF_CONN_ASYNC(self, set_isolation_level); - EXC_IF_TPC_PREPARED(self, set_isolation_level); + _set_session_checks(self, set_isolation_level); if (!PyArg_ParseTuple(args, "i", &level)) return NULL; - if (level < 0 || level > 4) { + if (level < 0 || level > 5) { PyErr_SetString(PyExc_ValueError, "isolation level must be between 0 and 4"); return NULL; } - if (conn_switch_isolation_level(self, level) < 0) { - return NULL; + if (level == 0) { + if (0 > conn_set_session(self, 1, + ISOLATION_LEVEL_DEFAULT, self->readonly, self->deferrable)) { + return NULL; + } + } + else { + if (0 > conn_set_session(self, 0, + level, self->readonly, self->deferrable)) { + return NULL; + } } Py_RETURN_NONE; @@ -1107,6 +1130,9 @@ connection_setup(connectionObject *self, const char *dsn, long int async) self->async_status = ASYNC_DONE; if (!(self->string_types = PyDict_New())) { goto exit; } if (!(self->binary_types = PyDict_New())) { goto exit; } + self->isolevel = ISOLATION_LEVEL_DEFAULT; + self->readonly = STATE_DEFAULT; + self->deferrable = STATE_DEFAULT; /* other fields have been zeroed by tp_alloc */ pthread_mutex_init(&(self->lock), NULL); diff --git a/psycopg/pqpath.c b/psycopg/pqpath.c index 328a2b26..50bd5201 100644 --- a/psycopg/pqpath.c +++ b/psycopg/pqpath.c @@ -53,7 +53,9 @@ #endif extern HIDDEN PyObject *psyco_DescriptionType; - +extern HIDDEN const char *srv_isolevels[]; +extern HIDDEN const char *srv_readonly[]; +extern HIDDEN const char *srv_deferrable[]; /* Strip off the severity from a Postgres error message. */ static const char * @@ -479,6 +481,8 @@ int pq_begin_locked(connectionObject *conn, PGresult **pgres, char **error, PyThreadState **tstate) { + const size_t bufsize = 256; + char buf[bufsize]; int result; Dprintf("pq_begin_locked: pgconn = %p, autocommit = %d, status = %d", @@ -489,7 +493,24 @@ pq_begin_locked(connectionObject *conn, PGresult **pgres, char **error, return 0; } - result = pq_execute_command_locked(conn, "BEGIN", pgres, error, tstate); + if (conn->isolevel == ISOLATION_LEVEL_DEFAULT + && conn->readonly == STATE_DEFAULT + && conn->deferrable == STATE_DEFAULT) { + strcpy(buf, "BEGIN"); + } + else { + snprintf(buf, bufsize, + conn->server_version >= 80000 ? + "BEGIN%s%s%s%s" : "BEGIN;SET TRANSACTION%s%s%s%s", + (conn->isolevel >= 1 && conn->isolevel <= 4) + ? " ISOLATION LEVEL " : "", + (conn->isolevel >= 1 && conn->isolevel <= 4) + ? srv_isolevels[conn->isolevel] : "", + srv_readonly[conn->readonly], + srv_deferrable[conn->deferrable]); + } + + result = pq_execute_command_locked(conn, buf, pgres, error, tstate); if (result == 0) conn->status = CONN_STATUS_BEGIN; diff --git a/tests/test_connection.py b/tests/test_connection.py index 4b72be14..703d8f14 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -488,7 +488,7 @@ class IsolationLevelsTestCase(ConnectingTestCase): conn = self.connect() self.assertEqual( conn.isolation_level, - psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) + psycopg2.extensions.ISOLATION_LEVEL_DEFAULT) def test_encoding(self): conn = self.connect() @@ -522,14 +522,33 @@ class IsolationLevelsTestCase(ConnectingTestCase): got_name = curs.fetchone()[0] if name is None: - curs.execute('show default_transaction_isolation;') + curs.execute('show transaction_isolation;') name = curs.fetchone()[0] self.assertEqual(name, got_name) conn.commit() self.assertRaises(ValueError, conn.set_isolation_level, -1) - self.assertRaises(ValueError, conn.set_isolation_level, 5) + self.assertRaises(ValueError, conn.set_isolation_level, 6) + + def test_set_isolation_level_default(self): + conn = self.connect() + curs = conn.cursor() + + conn.autocommit = True + curs.execute("set default_transaction_isolation to 'read committed'") + + conn.autocommit = False + conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + self.assertEqual(conn.isolation_level, + psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + curs.execute("show transaction_isolation") + self.assertEqual(curs.fetchone()[0], "serializable") + + conn.rollback() + conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_DEFAULT) + curs.execute("show transaction_isolation") + self.assertEqual(curs.fetchone()[0], "read committed") def test_set_isolation_level_abort(self): conn = self.connect() @@ -541,32 +560,14 @@ class IsolationLevelsTestCase(ConnectingTestCase): self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_INTRANS, conn.get_transaction_status()) - conn.set_isolation_level( + # changed in psycopg 2.7 + self.assertRaises(psycopg2.ProgrammingError, + conn.set_isolation_level, psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) - self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, - conn.get_transaction_status()) - cur.execute("select count(*) from isolevel;") - self.assertEqual(0, cur.fetchone()[0]) - - cur.execute("insert into isolevel values (10);") self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_INTRANS, conn.get_transaction_status()) - conn.set_isolation_level( - psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) - self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, - conn.get_transaction_status()) - cur.execute("select count(*) from isolevel;") - self.assertEqual(0, cur.fetchone()[0]) - - cur.execute("insert into isolevel values (10);") - self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, - conn.get_transaction_status()) - conn.set_isolation_level( - psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) - self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, - conn.get_transaction_status()) - cur.execute("select count(*) from isolevel;") - self.assertEqual(1, cur.fetchone()[0]) + self.assertEqual(conn.isolation_level, + psycopg2.extensions.ISOLATION_LEVEL_DEFAULT) def test_isolation_level_autocommit(self): cnn1 = self.connect() @@ -1042,13 +1043,13 @@ class TransactionControlTests(ConnectingTestCase): cur = self.conn.cursor() self.conn.set_session( psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) - cur.execute("SHOW default_transaction_isolation;") + cur.execute("SHOW transaction_isolation;") self.assertEqual(cur.fetchone()[0], 'serializable') self.conn.rollback() self.conn.set_session( psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ) - cur.execute("SHOW default_transaction_isolation;") + cur.execute("SHOW transaction_isolation;") if self.conn.server_version > 80000: self.assertEqual(cur.fetchone()[0], 'repeatable read') else: @@ -1057,13 +1058,13 @@ class TransactionControlTests(ConnectingTestCase): self.conn.set_session( isolation_level=psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) - cur.execute("SHOW default_transaction_isolation;") + cur.execute("SHOW transaction_isolation;") self.assertEqual(cur.fetchone()[0], 'read committed') self.conn.rollback() self.conn.set_session( isolation_level=psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED) - cur.execute("SHOW default_transaction_isolation;") + cur.execute("SHOW transaction_isolation;") if self.conn.server_version > 80000: self.assertEqual(cur.fetchone()[0], 'read uncommitted') else: @@ -1073,12 +1074,12 @@ class TransactionControlTests(ConnectingTestCase): def test_set_isolation_level_str(self): cur = self.conn.cursor() self.conn.set_session("serializable") - cur.execute("SHOW default_transaction_isolation;") + cur.execute("SHOW transaction_isolation;") self.assertEqual(cur.fetchone()[0], 'serializable') self.conn.rollback() self.conn.set_session("repeatable read") - cur.execute("SHOW default_transaction_isolation;") + cur.execute("SHOW transaction_isolation;") if self.conn.server_version > 80000: self.assertEqual(cur.fetchone()[0], 'repeatable read') else: @@ -1086,12 +1087,12 @@ class TransactionControlTests(ConnectingTestCase): self.conn.rollback() self.conn.set_session("read committed") - cur.execute("SHOW default_transaction_isolation;") + cur.execute("SHOW transaction_isolation;") self.assertEqual(cur.fetchone()[0], 'read committed') self.conn.rollback() self.conn.set_session("read uncommitted") - cur.execute("SHOW default_transaction_isolation;") + cur.execute("SHOW transaction_isolation;") if self.conn.server_version > 80000: self.assertEqual(cur.fetchone()[0], 'read uncommitted') else: @@ -1106,57 +1107,57 @@ class TransactionControlTests(ConnectingTestCase): def test_set_read_only(self): cur = self.conn.cursor() self.conn.set_session(readonly=True) - cur.execute("SHOW default_transaction_read_only;") + cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'on') self.conn.rollback() - cur.execute("SHOW default_transaction_read_only;") + cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'on') self.conn.rollback() cur = self.conn.cursor() self.conn.set_session(readonly=None) - cur.execute("SHOW default_transaction_read_only;") + cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'on') self.conn.rollback() self.conn.set_session(readonly=False) - cur.execute("SHOW default_transaction_read_only;") + cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'off') self.conn.rollback() def test_set_default(self): cur = self.conn.cursor() - cur.execute("SHOW default_transaction_isolation;") - default_isolevel = cur.fetchone()[0] - cur.execute("SHOW default_transaction_read_only;") - default_readonly = cur.fetchone()[0] + cur.execute("SHOW transaction_isolation;") + isolevel = cur.fetchone()[0] + cur.execute("SHOW transaction_read_only;") + readonly = cur.fetchone()[0] self.conn.rollback() self.conn.set_session(isolation_level='serializable', readonly=True) self.conn.set_session(isolation_level='default', readonly='default') - cur.execute("SHOW default_transaction_isolation;") - self.assertEqual(cur.fetchone()[0], default_isolevel) - cur.execute("SHOW default_transaction_read_only;") - self.assertEqual(cur.fetchone()[0], default_readonly) + cur.execute("SHOW transaction_isolation;") + self.assertEqual(cur.fetchone()[0], isolevel) + cur.execute("SHOW transaction_read_only;") + self.assertEqual(cur.fetchone()[0], readonly) @skip_before_postgres(9, 1) def test_set_deferrable(self): cur = self.conn.cursor() self.conn.set_session(readonly=True, deferrable=True) - cur.execute("SHOW default_transaction_read_only;") + cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'on') - cur.execute("SHOW default_transaction_deferrable;") + cur.execute("SHOW transaction_deferrable;") self.assertEqual(cur.fetchone()[0], 'on') self.conn.rollback() - cur.execute("SHOW default_transaction_deferrable;") + cur.execute("SHOW transaction_deferrable;") self.assertEqual(cur.fetchone()[0], 'on') self.conn.rollback() self.conn.set_session(deferrable=False) - cur.execute("SHOW default_transaction_read_only;") + cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'on') - cur.execute("SHOW default_transaction_deferrable;") + cur.execute("SHOW transaction_deferrable;") self.assertEqual(cur.fetchone()[0], 'off') self.conn.rollback() @@ -1258,9 +1259,9 @@ class AutocommitTests(ConnectingTestCase): self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), psycopg2.extensions.TRANSACTION_STATUS_IDLE) - cur.execute("SHOW default_transaction_isolation;") + cur.execute("SHOW transaction_isolation;") self.assertEqual(cur.fetchone()[0], 'serializable') - cur.execute("SHOW default_transaction_read_only;") + cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'on')