'with' starts a transaction even on autocommit connections

Close #941
This commit is contained in:
Daniele Varrazzo 2021-05-24 14:13:19 +02:00
parent d8e6426433
commit e5ad0ab2d9
6 changed files with 127 additions and 12 deletions

2
NEWS
View File

@ -5,6 +5,8 @@ What's new in psycopg 2.9
------------------------- -------------------------
- Dropped support for Python 2.7, 3.4, 3.5 (:tickets:`#1198, #1000, #1197`). - Dropped support for Python 2.7, 3.4, 3.5 (:tickets:`#1198, #1000, #1197`).
- ``with connection`` starts a transaction on autocommit transactions too
(:ticket:`#941`).
- Connection exceptions with sqlstate ``08XXX`` reclassified as - Connection exceptions with sqlstate ``08XXX`` reclassified as
`~psycopg2.OperationalError` (a subclass of the previously used `~psycopg2.OperationalError` (a subclass of the previously used
`~psycopg2.DatabaseError`) (:ticket:`#1148`). `~psycopg2.DatabaseError`) (:ticket:`#1148`).

View File

@ -832,6 +832,9 @@ and each ``with`` block is effectively wrapped in a separate transaction::
finally: finally:
conn.close() conn.close()
.. versionchanged:: 2.9
``with connection`` starts a transaction also on autocommit connections.
.. index:: .. index::
pair: Server side; Cursor pair: Server side; Cursor

View File

@ -145,6 +145,9 @@ struct connectionObject {
/* the pid this connection was created into */ /* the pid this connection was created into */
pid_t procpid; pid_t procpid;
/* inside a with block */
int entered;
}; };
/* map isolation level values into a numeric const */ /* map isolation level values into a numeric const */

View File

@ -406,10 +406,22 @@ psyco_conn_tpc_recover(connectionObject *self, PyObject *dummy)
static PyObject * static PyObject *
psyco_conn_enter(connectionObject *self, PyObject *dummy) psyco_conn_enter(connectionObject *self, PyObject *dummy)
{ {
PyObject *rv = NULL;
EXC_IF_CONN_CLOSED(self); EXC_IF_CONN_CLOSED(self);
if (self->entered) {
PyErr_SetString(ProgrammingError,
"the connection cannot be re-entered recursively");
goto exit;
}
self->entered = 1;
Py_INCREF(self); Py_INCREF(self);
return (PyObject *)self; rv = (PyObject *)self;
exit:
return rv;
} }
@ -427,6 +439,9 @@ psyco_conn_exit(connectionObject *self, PyObject *args)
goto exit; goto exit;
} }
/* even if there will be an error, consider ourselves out */
self->entered = 0;
if (type == Py_None) { if (type == Py_None) {
if (!(tmp = PyObject_CallMethod((PyObject *)self, "commit", NULL))) { if (!(tmp = PyObject_CallMethod((PyObject *)self, "commit", NULL))) {
goto exit; goto exit;

View File

@ -347,14 +347,19 @@ pq_begin_locked(connectionObject *conn, PyThreadState **tstate)
char buf[256]; /* buf size must be same as bufsize */ char buf[256]; /* buf size must be same as bufsize */
int result; int result;
Dprintf("pq_begin_locked: pgconn = %p, autocommit = %d, status = %d", Dprintf("pq_begin_locked: pgconn = %p, %d, status = %d",
conn->pgconn, conn->autocommit, conn->status); conn->pgconn, conn->autocommit, conn->status);
if (conn->autocommit || conn->status != CONN_STATUS_READY) { if (conn->status != CONN_STATUS_READY) {
Dprintf("pq_begin_locked: transaction in progress"); Dprintf("pq_begin_locked: transaction in progress");
return 0; return 0;
} }
if (conn->autocommit && !conn->entered) {
Dprintf("pq_begin_locked: autocommit and no with block");
return 0;
}
if (conn->isolevel == ISOLATION_LEVEL_DEFAULT if (conn->isolevel == ISOLATION_LEVEL_DEFAULT
&& conn->readonly == STATE_DEFAULT && conn->readonly == STATE_DEFAULT
&& conn->deferrable == STATE_DEFAULT) { && conn->deferrable == STATE_DEFAULT) {
@ -393,10 +398,10 @@ pq_commit(connectionObject *conn)
Py_BEGIN_ALLOW_THREADS; Py_BEGIN_ALLOW_THREADS;
pthread_mutex_lock(&conn->lock); pthread_mutex_lock(&conn->lock);
Dprintf("pq_commit: pgconn = %p, autocommit = %d, status = %d", Dprintf("pq_commit: pgconn = %p, status = %d",
conn->pgconn, conn->autocommit, conn->status); conn->pgconn, conn->status);
if (conn->autocommit || conn->status != CONN_STATUS_BEGIN) { if (conn->status != CONN_STATUS_BEGIN) {
Dprintf("pq_commit: no transaction to commit"); Dprintf("pq_commit: no transaction to commit");
retvalue = 0; retvalue = 0;
} }
@ -427,10 +432,10 @@ pq_abort_locked(connectionObject *conn, PyThreadState **tstate)
{ {
int retvalue = -1; int retvalue = -1;
Dprintf("pq_abort_locked: pgconn = %p, autocommit = %d, status = %d", Dprintf("pq_abort_locked: pgconn = %p, status = %d",
conn->pgconn, conn->autocommit, conn->status); conn->pgconn, conn->status);
if (conn->autocommit || conn->status != CONN_STATUS_BEGIN) { if (conn->status != CONN_STATUS_BEGIN) {
Dprintf("pq_abort_locked: no transaction to abort"); Dprintf("pq_abort_locked: no transaction to abort");
return 0; return 0;
} }
@ -488,12 +493,12 @@ pq_reset_locked(connectionObject *conn, PyThreadState **tstate)
{ {
int retvalue = -1; int retvalue = -1;
Dprintf("pq_reset_locked: pgconn = %p, autocommit = %d, status = %d", Dprintf("pq_reset_locked: pgconn = %p, status = %d",
conn->pgconn, conn->autocommit, conn->status); conn->pgconn, conn->status);
conn->mark += 1; conn->mark += 1;
if (!conn->autocommit && conn->status == CONN_STATUS_BEGIN) { if (conn->status == CONN_STATUS_BEGIN) {
retvalue = pq_execute_command_locked(conn, "ABORT", tstate); retvalue = pq_execute_command_locked(conn, "ABORT", tstate);
if (retvalue != 0) return retvalue; if (retvalue != 0) return retvalue;
} }

View File

@ -155,6 +155,93 @@ class WithConnectionTestCase(WithTestCase):
curs.execute("select * from test_with") curs.execute("select * from test_with")
self.assertEqual(curs.fetchall(), []) self.assertEqual(curs.fetchall(), [])
def test_cant_reenter(self):
raised_ok = False
with self.conn:
try:
with self.conn:
pass
except psycopg2.ProgrammingError:
raised_ok = True
self.assert_(raised_ok)
# Still good
with self.conn:
pass
def test_with_autocommit(self):
self.conn.autocommit = True
self.assertEqual(
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
)
with self.conn:
curs = self.conn.cursor()
curs.execute("insert into test_with values (1)")
self.assertEqual(
self.conn.info.transaction_status,
ext.TRANSACTION_STATUS_INTRANS,
)
self.assertEqual(
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
)
curs.execute("select count(*) from test_with")
self.assertEqual(curs.fetchone()[0], 1)
self.assertEqual(
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
)
def test_with_autocommit_pyerror(self):
self.conn.autocommit = True
raised_ok = False
try:
with self.conn:
curs = self.conn.cursor()
curs.execute("insert into test_with values (2)")
self.assertEqual(
self.conn.info.transaction_status,
ext.TRANSACTION_STATUS_INTRANS,
)
1 / 0
except ZeroDivisionError:
raised_ok = True
self.assert_(raised_ok)
self.assertEqual(
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
)
curs.execute("select count(*) from test_with")
self.assertEqual(curs.fetchone()[0], 0)
self.assertEqual(
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
)
def test_with_autocommit_pgerror(self):
self.conn.autocommit = True
raised_ok = False
try:
with self.conn:
curs = self.conn.cursor()
curs.execute("insert into test_with values (2)")
self.assertEqual(
self.conn.info.transaction_status,
ext.TRANSACTION_STATUS_INTRANS,
)
curs.execute("insert into test_with values ('x')")
except psycopg2.errors.InvalidTextRepresentation:
raised_ok = True
self.assert_(raised_ok)
self.assertEqual(
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
)
curs.execute("select count(*) from test_with")
self.assertEqual(curs.fetchone()[0], 0)
self.assertEqual(
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
)
class WithCursorTestCase(WithTestCase): class WithCursorTestCase(WithTestCase):
def test_with_ok(self): def test_with_ok(self):